Skip to content

Commit 7e33a6a

Browse files
committed
update dependencies, add state testing token validity
1 parent 7326c4b commit 7e33a6a

File tree

9 files changed

+108
-41
lines changed

9 files changed

+108
-41
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ repositories {
2929
jcenter()
3030
}
3131
32-
compile group: 'com.adamratzman', name: 'spotify-api-kotlin', version: '2.3.0'
32+
compile group: 'com.adamratzman', name: 'spotify-api-kotlin', version: '2.3.01'
3333
```
3434

3535
To use the latest snapshot instead, you must add the Jitpack repository as well
@@ -51,7 +51,7 @@ dependencies {
5151
<dependency>
5252
<groupId>com.adamratzman</groupId>
5353
<artifactId>spotify-api-kotlin</artifactId>
54-
<version>2.3.0</version>
54+
<version>2.3.01</version>
5555
</dependency>
5656
5757
<repository>

build.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ buildscript {
1414
plugins {
1515
id "com.diffplug.gradle.spotless" version "3.23.0"
1616
id "base"
17-
id "io.codearte.nexus-staging" version "0.20.0"
17+
id "io.codearte.nexus-staging" version "0.21.0"
1818
id "com.bmuschko.nexus" version "2.3.1"
1919
}
2020

2121
apply plugin: 'kotlin'
2222
apply plugin: 'org.jetbrains.dokka'
2323

2424
group 'com.adamratzman'
25-
version '2.3.0'
25+
version '2.3.01'
2626

2727
archivesBaseName = 'spotify-api-kotlin'
2828

@@ -39,7 +39,7 @@ dependencies {
3939
compile "com.squareup.moshi:moshi:1.8.0"
4040
compile "com.squareup.moshi:moshi-kotlin:1.8.0"
4141

42-
compile group: 'com.google.http-client', name: 'google-http-client', version: '1.23.0'
42+
compile group: 'com.google.http-client', name: 'google-http-client', version: '1.29.1'
4343

4444
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
4545
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.adamratzman.spotify.http.byteEncode
2424
import com.adamratzman.spotify.models.AuthenticationError
2525
import com.adamratzman.spotify.models.BadRequestException
2626
import com.adamratzman.spotify.models.Token
27+
import com.adamratzman.spotify.models.TokenValidityResponse
2728
import com.adamratzman.spotify.models.serialization.toObject
2829
import com.squareup.moshi.Moshi
2930
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
@@ -149,6 +150,26 @@ abstract class SpotifyAPI internal constructor(
149150
fun getAuthorizationUrl(vararg scopes: SpotifyScope, redirectUri: String): String {
150151
return getAuthUrlFull(*scopes, clientId = clientId, redirectUri = redirectUri)
151152
}
153+
154+
/**
155+
* Tests whether the current [token] is actually valid. By default, an endpoint is called *once* to verify
156+
* validity.
157+
*
158+
* @param makeTestRequest Whether to also make an endpoint request to verify authentication.
159+
*
160+
* @return [TokenValidityResponse] containing whether this token is valid, and if not, an Exception explaining why
161+
*/
162+
fun isTokenValid(makeTestRequest: Boolean = true): TokenValidityResponse {
163+
if (!token.shouldRefresh()) return TokenValidityResponse(false, SpotifyException("Token expired"))
164+
if (!makeTestRequest) return TokenValidityResponse(true, null)
165+
166+
return try {
167+
browse.getAvailableGenreSeeds().complete()
168+
TokenValidityResponse(true, null)
169+
} catch (e: Exception) {
170+
TokenValidityResponse(false, e)
171+
}
172+
}
152173
}
153174

154175
/**
@@ -365,6 +386,17 @@ class SpotifyClientAPI internal constructor(
365386
fun getAuthorizationUrl(vararg scopes: SpotifyScope): String {
366387
return getAuthUrlFull(*scopes, clientId = clientId, redirectUri = redirectUri)
367388
}
389+
390+
/**
391+
* Whether the current access token allows access to scope [scope]
392+
*/
393+
fun hasScope(scope: SpotifyScope): Boolean = hasScopes(scope)
394+
395+
/**
396+
* Whether the current access token allows access to all of the provided scopes
397+
*/
398+
fun hasScopes(scope: SpotifyScope, vararg scopes: SpotifyScope): Boolean =
399+
!isTokenValid(false).isValid && (scopes.toList() + scope).all { token.scopes.contains(it) }
368400
}
369401

370402
fun getAuthUrlFull(vararg scopes: SpotifyScope, clientId: String, redirectUri: String): String {

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

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,28 @@ import java.util.function.Supplier
1010
* Provides a uniform interface to retrieve, whether synchronously or asynchronously, [T] from Spotify
1111
*/
1212
open class SpotifyRestAction<T> internal constructor(protected val api: SpotifyAPI, private val supplier: Supplier<T>) {
13+
private var hasRunBacking: Boolean = false
14+
private var hasCompletedBacking: Boolean = false
15+
16+
/**
17+
* Whether this REST action has been *commenced*.
18+
*
19+
* Not to be confused with [hasCompleted]
20+
*/
21+
fun hasRun() = hasRunBacking
22+
23+
/**
24+
* Whether this REST action has been fully *completed*
25+
*/
26+
fun hasCompleted() = hasCompletedBacking
1327

1428
/**
1529
* Invoke [supplier] and synchronously retrieve [T]
1630
*/
1731
fun complete(): T {
32+
hasRunBacking = true
1833
return try {
19-
supplier.get()
34+
supplier.get().also { hasCompletedBacking = true }
2035
} catch (e: Throwable) {
2136
throw e
2237
}
@@ -25,22 +40,23 @@ open class SpotifyRestAction<T> internal constructor(protected val api: SpotifyA
2540
/**
2641
* Invoke [supplier] asynchronously with no consumer
2742
*/
28-
fun queue() = queue({}, { throw it })
43+
fun queue(): SpotifyRestAction<T> = queue({}, { throw it })
2944

3045
/**
3146
* Invoke [supplier] asynchronously and consume [consumer] with the [T] value returned
3247
*
3348
* @param consumer to be invoked with [T] after successful completion of [supplier]
3449
*/
35-
fun queue(consumer: (T) -> Unit) = queue(consumer, {})
50+
fun queue(consumer: (T) -> Unit): SpotifyRestAction<T> = queue(consumer, {})
3651

3752
/**
3853
* Invoke [supplier] asynchronously and consume [consumer] with the [T] value returned
3954
*
4055
* @param failure Consumer to invoke when an exception is thrown by [supplier]
4156
* @param consumer to be invoked with [T] after successful completion of [supplier]
4257
*/
43-
fun queue(consumer: ((T) -> Unit), failure: ((Throwable) -> Unit)) {
58+
fun queue(consumer: ((T) -> Unit), failure: ((Throwable) -> Unit)): SpotifyRestAction<T> {
59+
hasRunBacking = true
4460
api.executor.execute {
4561
try {
4662
val result = complete()
@@ -49,12 +65,13 @@ open class SpotifyRestAction<T> internal constructor(protected val api: SpotifyA
4965
failure(t)
5066
}
5167
}
68+
return this
5269
}
5370

5471
/**
5572
* Return [supplier] as a [CompletableFuture]
5673
*/
57-
fun asFuture() = CompletableFuture.supplyAsync(supplier)
74+
fun asFuture(): CompletableFuture<T> = CompletableFuture.supplyAsync(supplier)
5875

5976
/**
6077
* Invoke [supplier] asynchronously immediately and invoke [consumer] after the specified quantity of time
@@ -63,11 +80,12 @@ open class SpotifyRestAction<T> internal constructor(protected val api: SpotifyA
6380
* @param timeUnit the unit that [quantity] is in
6481
* @param consumer to be invoked with [T] after successful completion of [supplier]
6582
*/
66-
fun queueAfter(quantity: Int, timeUnit: TimeUnit = TimeUnit.SECONDS, consumer: (T) -> Unit) {
83+
fun queueAfter(quantity: Int, timeUnit: TimeUnit = TimeUnit.SECONDS, consumer: (T) -> Unit): SpotifyRestAction<T> {
6784
val runAt = System.currentTimeMillis() + timeUnit.toMillis(quantity.toLong())
6885
queue { result ->
6986
api.executor.schedule({ consumer(result) }, runAt - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
7087
}
88+
return this
7189
}
7290

7391
override fun toString() = complete().toString()

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

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.adamratzman.spotify.models.AbstractPagingObject
1010
import com.adamratzman.spotify.models.BadRequestException
1111
import com.adamratzman.spotify.models.ErrorObject
1212
import com.adamratzman.spotify.models.ErrorResponse
13+
import com.adamratzman.spotify.models.SpotifyAuthenticationException
1314
import com.adamratzman.spotify.models.serialization.toObject
1415
import java.net.HttpURLConnection
1516
import java.net.URLEncoder
@@ -33,21 +34,24 @@ abstract class SpotifyEndpoint(val api: SpotifyAPI) {
3334
}
3435

3536
internal fun delete(
36-
url: String,
37-
body: String? = null,
38-
contentType: String? = null
37+
url: String,
38+
body: String? = null,
39+
contentType: String? = null
3940
): String {
4041
return execute(url, body, HttpRequestMethod.DELETE, contentType = contentType)
4142
}
4243

4344
private fun execute(
44-
url: String,
45-
body: String? = null,
46-
method: HttpRequestMethod = HttpRequestMethod.GET,
47-
retry202: Boolean = true,
48-
contentType: String? = null
45+
url: String,
46+
body: String? = null,
47+
method: HttpRequestMethod = HttpRequestMethod.GET,
48+
retry202: Boolean = true,
49+
contentType: String? = null
4950
): String {
50-
if (api is SpotifyAppAPI && System.currentTimeMillis() >= api.expireTime) api.refreshToken()
51+
if (api is SpotifyAppAPI && System.currentTimeMillis() >= api.expireTime) {
52+
if (!api.automaticRefresh) throw SpotifyAuthenticationException("The access token has expired.")
53+
else api.refreshToken()
54+
}
5155

5256
val spotifyRequest = SpotifyRequest(url, method, body, api)
5357
val cacheState = if (api.useCache) cache[spotifyRequest] else null
@@ -73,15 +77,15 @@ abstract class SpotifyEndpoint(val api: SpotifyAPI) {
7377
}
7478

7579
private fun handleResponse(
76-
document: HttpResponse,
77-
cacheState: CacheState?,
78-
spotifyRequest: SpotifyRequest,
79-
retry202: Boolean
80+
document: HttpResponse,
81+
cacheState: CacheState?,
82+
spotifyRequest: SpotifyRequest,
83+
retry202: Boolean
8084
): String? {
8185
val statusCode = document.responseCode
8286

8387
if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
84-
if (cacheState?.eTag == null) throw BadRequestException("304 status only allowed on Etag-able endpoints")
88+
if (cacheState?.eTag == null) throw IllegalArgumentException("304 status only allowed on Etag-able endpoints")
8589
return cacheState.data
8690
}
8791

@@ -109,10 +113,10 @@ abstract class SpotifyEndpoint(val api: SpotifyAPI) {
109113
}
110114

111115
private fun createConnection(
112-
url: String,
113-
body: String? = null,
114-
method: HttpRequestMethod = HttpRequestMethod.GET,
115-
contentType: String? = null
116+
url: String,
117+
body: String? = null,
118+
method: HttpRequestMethod = HttpRequestMethod.GET,
119+
contentType: String? = null
116120
) = HttpConnection(
117121
url,
118122
method,
@@ -185,10 +189,10 @@ class SpotifyCache {
185189
}
186190

187191
data class SpotifyRequest(
188-
val url: String,
189-
val method: HttpRequestMethod,
190-
val body: String?,
191-
val api: SpotifyAPI
192+
val url: String,
193+
val method: HttpRequestMethod,
194+
val body: String?,
195+
val api: SpotifyAPI
192196
)
193197

194198
data class CacheState(val data: String, val eTag: String?, val expireBy: Long = 0) {

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.squareup.moshi.Json
1515
* null if the token was created using a method that does not support token refresh
1616
* @property scopes A list of scopes granted access for this [accessToken]. An
1717
* empty list means that the token can only be used to acces public information.
18+
* @property expiresAt The time, in milliseconds, at which this Token expires
1819
*/
1920
data class Token(
2021
@Json(name = "access_token") val accessToken: String,
@@ -25,4 +26,10 @@ data class Token(
2526
@Transient val scopes: List<SpotifyScope> = scopeString?.let { str ->
2627
str.split(" ").mapNotNull { scope -> SpotifyScope.values().find { it.uri.equals(scope, true) } }
2728
} ?: listOf()
28-
)
29+
) {
30+
@Transient val expiresAt: Long = System.currentTimeMillis() + expiresIn * 1000
31+
32+
fun shouldRefresh(): Boolean = System.currentTimeMillis() > expiresAt
33+
}
34+
35+
data class TokenValidityResponse(val isValid: Boolean, val exception: Exception?)

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
/* Spotify Web API - Kotlin Wrapper; MIT License, 2019; Original author: Adam Ratzman */
2-
@file:Suppress("UNCHECKED_CAST")
3-
42
package com.adamratzman.spotify.models
53

64
import com.adamratzman.spotify.SpotifyAPI
@@ -73,6 +71,7 @@ class PagingObject<T>(
7371
}
7472
})
7573

74+
@Suppress("UNCHECKED_CAST")
7675
override fun getImpl(type: PagingTraversalType): AbstractPagingObject<T>? {
7776
return (if (type == PagingTraversalType.FORWARDS) next else previous)?.let { endpoint.get(it) }?.let { json ->
7877
when (itemClazz) {
@@ -113,6 +112,7 @@ class PagingObject<T>(
113112
/**
114113
* Get all PagingObjects associated with the request
115114
*/
115+
@Suppress("UNCHECKED_CAST")
116116
fun getAll() = endpoint.toAction(Supplier { (getAllImpl() as Sequence<PagingObject<T>>).toList() })
117117

118118
/**
@@ -153,6 +153,7 @@ class CursorBasedPagingObject<T>(
153153
/**
154154
* Get all CursorBasedPagingObjects associated with the request
155155
*/
156+
@Suppress("UNCHECKED_CAST")
156157
fun getAll() = endpoint.toAction(Supplier {
157158
getAllImpl() as Sequence<CursorBasedPagingObject<T>>
158159
})
@@ -164,6 +165,7 @@ class CursorBasedPagingObject<T>(
164165
getAll().complete().map { it.items }.flatten().toList()
165166
})
166167

168+
@Suppress("UNCHECKED_CAST")
167169
override fun getImpl(type: PagingTraversalType): AbstractPagingObject<T>? {
168170
if (type == PagingTraversalType.BACKWARDS) {
169171
throw IllegalArgumentException("CursorBasedPagingObjects only can go forwards")

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ abstract class CoreObject(
3737
@Transient val uri: SpotifyUri,
3838
_externalUrls: Map<String, String>
3939
) : Identifiable(_href, _id) {
40-
@Transient val externalUrls: List<ExternalUrl> = _externalUrls.map { ExternalUrl(it.key, it.value) }
40+
@Transient
41+
val externalUrls: List<ExternalUrl> = _externalUrls.map { ExternalUrl(it.key, it.value) }
4142
}
4243

4344
abstract class RelinkingAvailableResponse(
@@ -90,6 +91,8 @@ data class ErrorResponse(val error: ErrorObject)
9091
*/
9192
data class ErrorObject(val status: Int, val message: String)
9293

94+
class SpotifyAuthenticationException(message: String) : Exception(message)
95+
9396
data class AuthenticationError(
9497
val error: String,
9598
@Json(name = "error_description") val description: String
@@ -103,17 +106,17 @@ class SpotifyUriException(message: String) : BadRequestException(message)
103106
* @param time the time, in seconds, until the next request can be sent
104107
*/
105108
class SpotifyRatelimitedException(time: Long) :
106-
UnNullableException("Calls to the Spotify API have been ratelimited for $time seconds until ${System.currentTimeMillis() + time * 1000}ms")
109+
UnNullableException("Calls to the Spotify API have been ratelimited for $time seconds until ${System.currentTimeMillis() + time * 1000}ms")
107110

108111
abstract class UnNullableException(message: String) : SpotifyException(message)
109112

110113
/**
111114
* Thrown when a request fails
112115
*/
113-
open class BadRequestException(message: String) : SpotifyException(message) {
114-
constructor(error: ErrorObject) : this("Received Status Code ${error.status}. Error cause: ${error.message}")
116+
open class BadRequestException(message: String, val statusCode: Int? = null) : SpotifyException(message) {
117+
constructor(error: ErrorObject) : this("Received Status Code ${error.status}. Error cause: ${error.message}", error.status)
115118
constructor(authenticationError: AuthenticationError) :
116-
this("Authentication error: ${authenticationError.error}. Description: ${authenticationError.description}")
119+
this("Authentication error: ${authenticationError.error}. Description: ${authenticationError.description}", 401)
117120
}
118121

119122
typealias Market = CountryCode

src/main/kotlin/com/adamratzman/spotify/utils/MiscUtils.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal fun <T> catch(function: () -> T): T? {
1010
return try {
1111
function()
1212
} catch (e: BadRequestException) {
13+
if (e.statusCode != 400) throw e // we should only ignore the exception if it's a bad request (400)
1314
null
1415
}
1516
}

0 commit comments

Comments
 (0)