Skip to content

Commit c4321e9

Browse files
committed
refresh token only when necessary (and when available)
reduce core pool size to 0 add automatic refresh parameter to app api
1 parent c9453fc commit c4321e9

File tree

6 files changed

+203
-206
lines changed

6 files changed

+203
-206
lines changed

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ class SpotifyApiBuilder(
9090
*/
9191
fun token(token: Token?) = apply { this.token = token }
9292

93+
/*fun build(type: AuthorizationType, automaticRefresh: Boolean = true) {
94+
95+
}*/
96+
97+
/**
98+
*
99+
*/
93100
fun buildCredentialed() = spotifyApi {
94101
credentials {
95102
clientId = this@SpotifyApiBuilder.clientId
@@ -101,7 +108,7 @@ class SpotifyApiBuilder(
101108
}
102109
}.buildCredentialed()
103110

104-
fun buildClient(automaticRefresh: Boolean = false) = spotifyApi {
111+
fun buildClient(automaticRefresh: Boolean = true) = spotifyApi {
105112
credentials {
106113
clientId = this@SpotifyApiBuilder.clientId
107114
clientSecret = this@SpotifyApiBuilder.clientSecret
@@ -174,7 +181,7 @@ class SpotifyApiBuilderDsl {
174181

175182
fun buildCredentialedAsync(consumer: (SpotifyAPI) -> Unit) = Runnable { consumer(buildCredentialed()) }.run()
176183

177-
fun buildCredentialed(): SpotifyAPI {
184+
fun buildCredentialed(automaticRefresh: Boolean = true): SpotifyAPI {
178185
val clientId = credentials.clientId
179186
val clientSecret = credentials.clientSecret
180187
if ((clientId == null || clientSecret == null) && (authentication.token == null && authentication.tokenString == null)) {
@@ -183,7 +190,7 @@ class SpotifyApiBuilderDsl {
183190
return when {
184191
authentication.token != null -> {
185192
SpotifyAppAPI(clientId ?: "not-set", clientSecret
186-
?: "not-set", authentication.token!!, useCache)
193+
?: "not-set", authentication.token!!, useCache, automaticRefresh)
187194
}
188195
authentication.tokenString != null -> {
189196
SpotifyAppAPI(
@@ -193,14 +200,15 @@ class SpotifyApiBuilderDsl {
193200
authentication.tokenString!!, "client_credentials",
194201
60000, null, null
195202
),
196-
useCache
203+
useCache,
204+
automaticRefresh
197205
)
198206
}
199207
else -> try {
200208
if (clientId == null || clientSecret == null) throw IllegalArgumentException("Illegal credentials provided")
201209
val token = getCredentialedToken(clientId, clientSecret)
202210
?: throw IllegalArgumentException("Invalid credentials provided")
203-
SpotifyAppAPI(clientId, clientSecret, token, useCache)
211+
SpotifyAppAPI(clientId, clientSecret, token, useCache, automaticRefresh)
204212
} catch (e: Exception) {
205213
throw SpotifyException("Invalid credentials provided in the login process", e)
206214
}

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

Lines changed: 41 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ import com.adamratzman.spotify.http.HttpHeader
2020
import com.adamratzman.spotify.http.HttpRequestMethod
2121
import com.adamratzman.spotify.http.SpotifyEndpoint
2222
import com.adamratzman.spotify.http.byteEncode
23+
import com.adamratzman.spotify.models.AuthenticationError
24+
import com.adamratzman.spotify.models.BadRequestException
2325
import com.adamratzman.spotify.models.Token
2426
import com.adamratzman.spotify.models.serialization.getAlbumConverter
2527
import com.adamratzman.spotify.models.serialization.getFeaturedPlaylistsConverter
2628
import com.adamratzman.spotify.models.serialization.getPlaylistConverter
2729
import com.adamratzman.spotify.models.serialization.getPublicUserConverter
2830
import com.adamratzman.spotify.models.serialization.getSavedTrackConverter
29-
import com.adamratzman.spotify.models.serialization.toObjectNullable
31+
import com.adamratzman.spotify.models.serialization.toObject
3032
import com.beust.klaxon.Klaxon
3133
import java.util.concurrent.Executors
3234
import java.util.concurrent.ScheduledFuture
@@ -59,7 +61,8 @@ abstract class SpotifyAPI internal constructor(
5961
val clientId: String,
6062
val clientSecret: String,
6163
var token: Token,
62-
useCache: Boolean
64+
useCache: Boolean,
65+
var automaticRefresh: Boolean
6366
) {
6467
private var refreshFuture: ScheduledFuture<*>? = null
6568

@@ -72,7 +75,7 @@ abstract class SpotifyAPI internal constructor(
7275
}
7376

7477
internal var expireTime = System.currentTimeMillis() + token.expiresIn * 1000
75-
internal val executor = Executors.newScheduledThreadPool(2)
78+
internal val executor = Executors.newScheduledThreadPool(0)
7679

7780
abstract val search: SearchAPI
7881
abstract val albums: AlbumAPI
@@ -91,9 +94,10 @@ abstract class SpotifyAPI internal constructor(
9194
* If the method used to create the [token] supports token refresh and
9295
* the information in [token] is accurate, attempt to refresh the token
9396
*
94-
* @return The old access token if refresh was successful, otherwise null
97+
* @return The old access token if refresh was successful
98+
* @throws BadRequestException if refresh fails
9599
*/
96-
abstract fun refreshToken(): Token?
100+
abstract fun refreshToken(): Token
97101

98102
/**
99103
* If the cache is enabled, clear all stored queries in the cache
@@ -147,8 +151,12 @@ abstract class SpotifyAPI internal constructor(
147151
* An API instance created with application credentials, not through
148152
* client authentication
149153
*/
150-
class SpotifyAppAPI internal constructor(clientId: String, clientSecret: String, token: Token, useCache: Boolean) :
151-
SpotifyAPI(clientId, clientSecret, token, useCache) {
154+
class SpotifyAppAPI internal constructor(clientId: String,
155+
clientSecret: String,
156+
token: Token,
157+
useCache: Boolean,
158+
automaticRefresh: Boolean) :
159+
SpotifyAPI(clientId, clientSecret, token, useCache, automaticRefresh) {
152160

153161
override val search: SearchAPI = SearchAPI(this)
154162
override val albums: AlbumAPI = AlbumAPI(this)
@@ -173,22 +181,16 @@ class SpotifyAppAPI internal constructor(clientId: String, clientSecret: String,
173181

174182
override val klaxon: Klaxon = getKlaxon(this)
175183

176-
init {
177-
if (clientId == "not-set" || clientSecret == "not-set") {
178-
logger.logWarning("Token refresh is disabled - application parameters not set")
179-
}
180-
}
181-
182-
override fun refreshToken(): Token? {
184+
override fun refreshToken(): Token {
183185
if (clientId != "not-set" && clientSecret != "not-set") {
184186
val currentToken = this.token
185187

186-
getCredentialedToken(clientId, clientSecret)?.let { token = it }
188+
token = getCredentialedToken(clientId, clientSecret)
187189
expireTime = System.currentTimeMillis() + token.expiresIn * 1000
188190

189191
return currentToken
190192
}
191-
return null
193+
throw BadRequestException("Either the client id or the client secret is not set")
192194
}
193195

194196
override fun clearCache() = clearAllCaches(
@@ -225,7 +227,7 @@ class SpotifyClientAPI internal constructor(
225227
automaticRefresh: Boolean = false,
226228
var redirectUri: String,
227229
useCache: Boolean
228-
) : SpotifyAPI(clientId, clientSecret, token, useCache) {
230+
) : SpotifyAPI(clientId, clientSecret, token, useCache, automaticRefresh) {
229231
override val search: SearchAPI = SearchAPI(this)
230232
override val albums: AlbumAPI = AlbumAPI(this)
231233
override val browse: BrowseAPI = BrowseAPI(this)
@@ -285,57 +287,37 @@ class SpotifyClientAPI internal constructor(
285287
val userId: String
286288

287289
init {
288-
init(automaticRefresh)
289290
userId = users.getUserProfile().complete().id
290291
}
291292

292-
private fun init(automaticRefresh: Boolean) {
293-
if (automaticRefresh) {
294-
if (clientId != "not-set" && clientSecret != "not-set" && redirectUri != "not-set") {
295-
if (token.expiresIn > 60) {
296-
executor.scheduleAtFixedRate(
297-
{ refreshToken() },
298-
(token.expiresIn - 30).toLong(),
299-
(token.expiresIn - 30).toLong(),
300-
TimeUnit.SECONDS
301-
)
302-
} else {
303-
refreshToken()
304-
init(automaticRefresh)
305-
}
306-
} else logger.logWarning("Automatic refresh unavailable - client parameters not set")
307-
}
308-
}
309-
310293
/**
311294
* Stop all automatic functions like refreshToken or clearCache and shut down the scheduled
312295
* executor
313296
* */
314297
fun cancelAutomatics() = executor.shutdown()
315298

316-
override fun refreshToken(): Token? {
299+
override fun refreshToken(): Token {
317300
val currentToken = this.token
318301

319-
val tempToken =
302+
val response =
320303
HttpConnection(
321304
url = "https://accounts.spotify.com/api/token",
322305
method = HttpRequestMethod.POST,
323306
body = "grant_type=refresh_token&refresh_token=${token.refreshToken ?: ""}",
324307
contentType = "application/x-www-form-urlencoded",
325308
api = this
326-
).execute(HttpHeader("Authorization", "Basic ${"$clientId:$clientSecret".byteEncode()}")).body
327-
.toObjectNullable<Token>(null)
328-
return if (tempToken?.accessToken == null) {
329-
logger.logWarning("Spotify token refresh failed")
330-
null
331-
} else {
309+
).execute(HttpHeader("Authorization", "Basic ${"$clientId:$clientSecret".byteEncode()}"))
310+
311+
if (response.responseCode / 200 == 1) {
312+
val tempToken = response.body.toObject<Token>(this)
332313
this.token = tempToken.copy(
333314
refreshToken = tempToken.refreshToken ?: this.token.refreshToken,
334315
scopes = tempToken.scopes
335316
)
336317
logger.logInfo("Successfully refreshed the Spotify token")
337-
currentToken
318+
return currentToken
338319
}
320+
else throw BadRequestException(response.body.toObject<AuthenticationError>(this))
339321
}
340322

341323
override fun clearCache() = clearAllCaches(
@@ -378,22 +360,26 @@ class SpotifyClientAPI internal constructor(
378360
}
379361
}
380362

381-
fun getAuthUrlFull(vararg scopes: SpotifyScope, clientId: String, redirectUri: String): String {
363+
fun getAuthUrlFull(vararg scopes: SpotifyScope, clientId: String, redirectUri: String): String {
382364
return "https://accounts.spotify.com/authorize/?client_id=$clientId" +
383365
"&response_type=code" +
384366
"&redirect_uri=$redirectUri" +
385367
if (scopes.isEmpty()) "" else "&scope=${scopes.joinToString("%20") { it.uri }}"
386368
}
387369

388-
fun getCredentialedToken(clientId: String, clientSecret: String) =
389-
HttpConnection(
390-
url = "https://accounts.spotify.com/api/token",
391-
method = HttpRequestMethod.POST,
392-
body = "grant_type=client_credentials",
393-
contentType = "application/x-www-form-urlencoded",
394-
api = null
395-
).execute(HttpHeader("Authorization", "Basic ${"$clientId:$clientSecret".byteEncode()}")).body
396-
.toObjectNullable<Token>(null)
370+
fun getCredentialedToken(clientId: String, clientSecret: String): Token {
371+
val response = HttpConnection(
372+
url = "https://accounts.spotify.com/api/token",
373+
method = HttpRequestMethod.POST,
374+
body = "grant_type=client_credentials",
375+
contentType = "application/x-www-form-urlencoded",
376+
api = null
377+
).execute(HttpHeader("Authorization", "Basic ${"$clientId:$clientSecret".byteEncode()}"))
378+
379+
if (response.responseCode / 200 == 1) return response.body.toObject(null)
380+
381+
throw BadRequestException(response.body.toObject<AuthenticationError>(null))
382+
}
397383

398384
private fun getKlaxon(api: SpotifyAPI) = Klaxon()
399385
.converter(getFeaturedPlaylistsConverter(api))

src/main/kotlin/com/adamratzman/spotify/http/HttpConnection.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ internal enum class HttpRequestMethod { GET, POST, PUT, DELETE }
99
internal data class HttpHeader(val key: String, val value: String)
1010

1111
internal class HttpConnection(
12-
private val url: String,
13-
private val method: HttpRequestMethod,
14-
private val body: String?,
15-
private val contentType: String?,
16-
private vararg val headers: HttpHeader,
17-
val api: SpotifyAPI? = null
12+
private val url: String,
13+
private val method: HttpRequestMethod,
14+
private val body: String?,
15+
private val contentType: String?,
16+
private vararg val headers: HttpHeader,
17+
val api: SpotifyAPI? = null
1818
) {
1919

2020
fun execute(vararg additionalHeaders: HttpHeader?, retryIf502: Boolean = true): HttpResponse {
@@ -52,6 +52,12 @@ internal class HttpConnection(
5252
text
5353
}
5454

55+
if (responseCode == 401 && body.contains("access token")
56+
&& api != null && api.automaticRefresh) {
57+
api.refreshToken()
58+
return execute(*additionalHeaders)
59+
}
60+
5561
return HttpResponse(
5662
responseCode = responseCode,
5763
body = body,

src/main/kotlin/com/adamratzman/spotify/models/ResultObjects.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,20 @@ data class ErrorResponse(val error: ErrorObject)
8989
*/
9090
data class ErrorObject(val status: Int, val message: String)
9191

92+
data class AuthenticationError(
93+
val error: String,
94+
@Json(name="error_description") val description: String
95+
)
96+
9297
class SpotifyUriException(message: String) : BadRequestException(message)
9398

9499
/**
95100
* Thrown when a request fails
96101
*/
97102
open class BadRequestException(message: String) : Exception(message) {
98103
constructor(error: ErrorObject) : this("Received Status Code ${error.status}. Error cause: ${error.message}")
104+
constructor(authenticationError: AuthenticationError)
105+
: this("Authentication error: ${authenticationError.error}. Description: ${authenticationError.description}")
99106
}
100107

101108
typealias Market = CountryCode

src/main/kotlin/com/adamratzman/spotify/models/SpotifyUris.kt

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,40 +52,30 @@ sealed class SpotifyUri(input: String, type: String) {
5252

5353
/**
5454
* Represents a Spotify **Album** URI, parsed from either a Spotify ID or taken from an endpoint.
55-
*
56-
* @property uri retrieve this URI as a string
57-
* @property id representation of this uri as an id
5855
*/
5956
class AlbumURI(input: String) : SpotifyUri(input, "album")
6057

6158
/**
6259
* Represents a Spotify **Artist** URI, parsed from either a Spotify ID or taken from an endpoint.
63-
*
64-
* @property uri retrieve this URI as a string
65-
* @property id representation of this uri as an id
6660
*/
6761
class ArtistURI(input: String) : SpotifyUri(input, "artist")
6862

6963
/**
7064
* Represents a Spotify **Track** URI, parsed from either a Spotify ID or taken from an endpoint.
71-
*
72-
* @property uri retrieve this URI as a string
73-
* @property id representation of this uri as an id
7465
*/
7566
class TrackURI(input: String) : SpotifyUri(input, "track")
7667

7768
/**
7869
* Represents a Spotify **User** URI, parsed from either a Spotify ID or taken from an endpoint.
79-
*
80-
* @property uri retrieve this URI as a string
81-
* @property id representation of this uri as an id
8270
*/
8371
class UserURI(input: String) : SpotifyUri(input, "user")
8472

8573
/**
8674
* Represents a Spotify **Playlist** URI, parsed from either a Spotify ID or taken from an endpoint.
87-
*
88-
* @property uri retrieve this URI as a string
89-
* @property id representation of this uri as an id
9075
*/
9176
class PlaylistURI(input: String) : SpotifyUri(input, "playlist")
77+
78+
/**
79+
* Represents a Spotify **local track** URI
80+
*/
81+
class LocalTrackURI(input: String) :SpotifyUri(input,"local")

0 commit comments

Comments
 (0)