Skip to content

Commit fc4542a

Browse files
committed
allow customizable refresh logic
1 parent 50110ff commit fc4542a

File tree

3 files changed

+88
-74
lines changed

3 files changed

+88
-74
lines changed

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -547,7 +547,8 @@ class SpotifyClientApiBuilder(
547547
options.defaultLimit,
548548
options.allowBulkRequests,
549549
options.requestTimeoutMillis,
550-
options.json
550+
options.json,
551+
options.refreshTokenProducer
551552
)
552553
} catch (e: CancellationException) {
553554
throw e
@@ -568,7 +569,8 @@ class SpotifyClientApiBuilder(
568569
options.defaultLimit,
569570
options.allowBulkRequests,
570571
options.requestTimeoutMillis,
571-
options.json
572+
options.json,
573+
options.refreshTokenProducer
572574
)
573575
authorization.tokenString != null -> SpotifyClientApi(
574576
clientId,
@@ -590,7 +592,8 @@ class SpotifyClientApiBuilder(
590592
options.defaultLimit,
591593
options.allowBulkRequests,
592594
options.requestTimeoutMillis,
593-
options.json
595+
options.json,
596+
options.refreshTokenProducer
594597
)
595598
else -> throw IllegalArgumentException(
596599
"At least one of: authorizationCode, tokenString, or token must be provided " +
@@ -634,7 +637,8 @@ class SpotifyAppApiBuilder(
634637
options.defaultLimit,
635638
options.allowBulkRequests,
636639
options.requestTimeoutMillis,
637-
options.json
640+
options.json,
641+
options.refreshTokenProducer
638642
)
639643
authorization.tokenString != null -> {
640644
SpotifyAppApi(
@@ -653,7 +657,8 @@ class SpotifyAppApiBuilder(
653657
options.defaultLimit,
654658
options.allowBulkRequests,
655659
options.requestTimeoutMillis,
656-
options.json
660+
options.json,
661+
options.refreshTokenProducer
657662
)
658663
}
659664
authorization.refreshTokenString != null -> {
@@ -670,7 +675,8 @@ class SpotifyAppApiBuilder(
670675
options.defaultLimit,
671676
options.allowBulkRequests,
672677
options.requestTimeoutMillis,
673-
options.json
678+
options.json,
679+
options.refreshTokenProducer
674680
)
675681
}
676682
else -> try {
@@ -689,7 +695,8 @@ class SpotifyAppApiBuilder(
689695
options.defaultLimit,
690696
options.allowBulkRequests,
691697
options.requestTimeoutMillis,
692-
options.json
698+
options.json,
699+
options.refreshTokenProducer
693700
)
694701
} catch (e: CancellationException) {
695702
throw e
@@ -770,6 +777,7 @@ data class SpotifyUserAuthorization(
770777
* @property enableAllOptions Whether to enable all provided utilities
771778
* @property allowBulkRequests Allow splitting too-large requests into smaller, allowable api requests
772779
* @property requestTimeoutMillis The maximum time, in milliseconds, before terminating an http request
780+
* @property refreshTokenProducer Provide if you want to use your own logic when refreshing a Spotify token
773781
*
774782
*/
775783
class SpotifyApiOptionsBuilder(
@@ -783,7 +791,8 @@ class SpotifyApiOptionsBuilder(
783791
var defaultLimit: Int = 50,
784792
var allowBulkRequests: Boolean = true,
785793
var requestTimeoutMillis: Long? = null,
786-
var json: Json = nonstrictJson
794+
var json: Json = nonstrictJson,
795+
var refreshTokenProducer: (suspend (SpotifyApi<*,*>) -> Token)? = null
787796
) {
788797
fun build() =
789798
if (enableAllOptions)
@@ -797,7 +806,8 @@ class SpotifyApiOptionsBuilder(
797806
defaultLimit = 50,
798807
allowBulkRequests = true,
799808
requestTimeoutMillis = requestTimeoutMillis,
800-
json = json
809+
json = json,
810+
refreshTokenProducer = refreshTokenProducer
801811
)
802812
else
803813
SpotifyApiOptions(
@@ -810,7 +820,8 @@ class SpotifyApiOptionsBuilder(
810820
defaultLimit,
811821
allowBulkRequests,
812822
requestTimeoutMillis,
813-
json
823+
json,
824+
refreshTokenProducer
814825
)
815826
}
816827

@@ -827,10 +838,11 @@ class SpotifyApiOptionsBuilder(
827838
* @property json The Json serializer/deserializer instance
828839
* @property allowBulkRequests Allow splitting too-large requests into smaller, allowable api requests
829840
* @property requestTimeoutMillis The maximum time, in milliseconds, before terminating an http request
841+
* @property refreshTokenProducer Provide if you want to use your own logic when refreshing a Spotify token
830842
*
831843
*/
832844

833-
data class SpotifyApiOptions(
845+
data class SpotifyApiOptions (
834846
var useCache: Boolean,
835847
var cacheLimit: Int?,
836848
var automaticRefresh: Boolean,
@@ -840,7 +852,8 @@ data class SpotifyApiOptions(
840852
var defaultLimit: Int,
841853
var allowBulkRequests: Boolean,
842854
var requestTimeoutMillis: Long?,
843-
var json: Json
855+
var json: Json,
856+
var refreshTokenProducer: (suspend (SpotifyApi<*,*>) -> Token)?
844857
)
845858

846859
@Deprecated("Name has been replaced by `options`", ReplaceWith("SpotifyApiOptions"))

src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt

Lines changed: 62 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ sealed class SpotifyApi<T : SpotifyApi<T, B>, B : ISpotifyApiBuilder<T, B>>(
7979
var defaultLimit: Int,
8080
var allowBulkRequests: Boolean,
8181
var requestTimeoutMillis: Long?,
82-
var json: Json
82+
var json: Json,
83+
var refreshTokenProducer: suspend (SpotifyApi<*,*>) -> Token
8384
) {
8485
var useCache = useCache
8586
set(value) {
@@ -232,7 +233,7 @@ sealed class SpotifyApi<T : SpotifyApi<T, B>, B : ISpotifyApiBuilder<T, B>>(
232233
* @return The old access token if refresh was successful
233234
* @throws BadRequestException if refresh fails
234235
*/
235-
abstract suspend fun suspendRefreshToken(): Token
236+
suspend fun suspendRefreshToken() = refreshTokenProducer(this).apply { this@SpotifyApi.token = this }
236237

237238
companion object {
238239
/*
@@ -301,7 +302,8 @@ class SpotifyAppApi internal constructor(
301302
defaultLimit: Int,
302303
allowBulkRequests: Boolean,
303304
requestTimeoutMillis: Long?,
304-
json: Json
305+
json: Json,
306+
refreshTokenProducer: (suspend (GenericSpotifyApi) -> Token)?
305307
) : SpotifyApi<SpotifyAppApi, SpotifyAppApiBuilder>(
306308
clientId,
307309
clientSecret,
@@ -315,7 +317,8 @@ class SpotifyAppApi internal constructor(
315317
defaultLimit,
316318
allowBulkRequests,
317319
requestTimeoutMillis,
318-
json
320+
json,
321+
refreshTokenProducer ?: defaultAppApiTokenRefreshProducer
319322
) {
320323
constructor(
321324
clientId: String,
@@ -335,7 +338,8 @@ class SpotifyAppApi internal constructor(
335338
options.defaultLimit,
336339
options.allowBulkRequests,
337340
options.requestTimeoutMillis,
338-
options.json
341+
options.json,
342+
options.refreshTokenProducer
339343
)
340344

341345
override val search: SearchApi = SearchApi(this)
@@ -359,15 +363,6 @@ class SpotifyAppApi internal constructor(
359363
*/
360364
override val following: FollowingApi = FollowingApi(this)
361365

362-
override suspend fun suspendRefreshToken(): Token {
363-
require(clientId != null && clientSecret != null) { "Either the client id or the client secret is not set" }
364-
val currentToken = this.token
365-
366-
token = getCredentialedToken(clientId, clientSecret, this, json)
367-
368-
return currentToken
369-
}
370-
371366
override val endpoints: List<SpotifyEndpoint>
372367
get() = listOf(
373368
search,
@@ -394,6 +389,14 @@ class SpotifyAppApi internal constructor(
394389

395390
useCache = this@SpotifyAppApi.useCache
396391
}
392+
393+
companion object {
394+
private val defaultAppApiTokenRefreshProducer: suspend (SpotifyApi<*,*>) -> Token = { api ->
395+
require(api.clientId != null && api.clientSecret != null) { "Either the client id or the client secret is not set" }
396+
397+
getCredentialedToken(api.clientId, api.clientSecret, api, api.json)
398+
}
399+
}
397400
}
398401

399402
/**
@@ -414,7 +417,8 @@ open class SpotifyClientApi internal constructor(
414417
defaultLimit: Int,
415418
allowBulkRequests: Boolean,
416419
requestTimeoutMillis: Long?,
417-
json: Json
420+
json: Json,
421+
refreshTokenProducer: (suspend (SpotifyApi<*, *>) -> Token)?
418422
) : SpotifyApi<SpotifyClientApi, SpotifyClientApiBuilder>(
419423
clientId,
420424
clientSecret,
@@ -428,7 +432,8 @@ open class SpotifyClientApi internal constructor(
428432
defaultLimit,
429433
allowBulkRequests,
430434
requestTimeoutMillis,
431-
json
435+
json,
436+
refreshTokenProducer ?: defaultClientApiTokenRefreshProducer
432437
) {
433438
constructor(
434439
clientId: String,
@@ -450,7 +455,8 @@ open class SpotifyClientApi internal constructor(
450455
options.defaultLimit,
451456
options.allowBulkRequests,
452457
options.requestTimeoutMillis,
453-
options.json
458+
options.json,
459+
options.refreshTokenProducer
454460
)
455461

456462
override val albums: AlbumApi = AlbumApi(this)
@@ -541,44 +547,6 @@ open class SpotifyClientApi internal constructor(
541547
runExecutableFunctions = false
542548
}
543549

544-
override suspend fun suspendRefreshToken(): Token {
545-
require(clientId != null && clientSecret != null) { "Either the client id or the client secret is not set" }
546-
547-
val currentToken = this.token
548-
549-
val response = executeTokenRequest(
550-
HttpConnection(
551-
"https://accounts.spotify.com/api/token",
552-
HttpRequestMethod.POST,
553-
mapOf(
554-
"grant_type" to "refresh_token",
555-
"refresh_token" to token.refreshToken
556-
),
557-
null,
558-
"application/x-www-form-urlencoded",
559-
listOf(),
560-
this
561-
), clientId, clientSecret
562-
)
563-
564-
if (response.responseCode / 200 == 1) {
565-
val tempToken = response.body.toObject(Token.serializer(), this, json)
566-
this.token = tempToken.copy(
567-
refreshToken = tempToken.refreshToken ?: this.token.refreshToken,
568-
scopeString = tempToken.scopeString
569-
)
570-
571-
logger.logInfo("Successfully refreshed the Spotify token")
572-
return currentToken
573-
} else throw BadRequestException(
574-
response.body.toObject(
575-
AuthenticationError.serializer(),
576-
this,
577-
json
578-
)
579-
)
580-
}
581-
582550
override val endpoints: List<SpotifyEndpoint>
583551
get() = listOf(
584552
search,
@@ -639,6 +607,40 @@ open class SpotifyClientApi internal constructor(
639607
else !isTokenValid(false).isValid &&
640608
token.scopes?.contains(scope) == true &&
641609
scopes.all { token.scopes?.contains(it) == true }
610+
611+
companion object {
612+
private val defaultClientApiTokenRefreshProducer: suspend (SpotifyApi<*,*>) -> Token = { api ->
613+
require(api.clientId != null && api.clientSecret != null) { "Either the client id or the client secret is not set" }
614+
615+
val currentToken = api.token
616+
617+
val response = executeTokenRequest(
618+
HttpConnection(
619+
"https://accounts.spotify.com/api/token",
620+
HttpRequestMethod.POST,
621+
mapOf(
622+
"grant_type" to "refresh_token",
623+
"refresh_token" to currentToken.refreshToken
624+
),
625+
null,
626+
"application/x-www-form-urlencoded",
627+
listOf(),
628+
api
629+
), api.clientId, api.clientSecret
630+
)
631+
632+
if (response.responseCode / 200 == 1) {
633+
api.logger.logInfo("Successfully refreshed the Spotify token")
634+
response.body.toObject(Token.serializer(), api, api.json)
635+
} else throw BadRequestException(
636+
response.body.toObject(
637+
AuthenticationError.serializer(),
638+
api,
639+
api.json
640+
)
641+
)
642+
}
643+
}
642644
}
643645

644646
/**
@@ -673,12 +675,9 @@ class SpotifyImplicitGrantApi(
673675
defaultLimit,
674676
allowBulkRequests,
675677
requestTimeoutMillis,
676-
json
677-
) {
678-
override suspend fun suspendRefreshToken(): Token {
679-
throw IllegalStateException("You cannot refresh an implicit grant access token!")
680-
}
681-
}
678+
json,
679+
{ throw IllegalStateException("You cannot refresh an implicit grant access token!") }
680+
)
682681

683682
@Deprecated("API name has been updated for kotlin convention consistency", ReplaceWith("SpotifyApi"))
684683
typealias SpotifyAPI<T, B> = SpotifyApi<T, B>
@@ -687,6 +686,8 @@ typealias SpotifyClientAPI = SpotifyClientApi
687686
@Deprecated("API name has been updated for kotlin convention consistency", ReplaceWith("SpotifyAppApi"))
688687
typealias SpotifyAppAPI = SpotifyAppApi
689688

689+
typealias GenericSpotifyApi = SpotifyApi<*, *>
690+
690691
/**
691692
*
692693
* Get an application token (can only access public methods) that can be used to instantiate a new [SpotifyAppApi]

src/commonTest/kotlin/com.adamratzman/spotify/priv/ShowApiTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ShowApiTest : Spek({
1919

2020
describe("get show") {
2121
it("known show") {
22-
val show = t.getShow("38bS44xjbVVZ3No3ByF1dJ").complete()!!
22+
val show = t.getShow("1iohmBNlRooIVtukKeavRa").complete()!!
2323
assertEquals("Love Letters", show.name)
2424
assertTrue(show.episodes.isNotEmpty())
2525
}

0 commit comments

Comments
 (0)