Skip to content

Commit 086d008

Browse files
committed
remove linked results, document more objects
Signed-off-by: Adam Ratzman <adam@adamratzman.com>
1 parent 8478cbe commit 086d008

File tree

11 files changed

+152
-81
lines changed

11 files changed

+152
-81
lines changed

src/main/kotlin/com/adamratzman/spotify/endpoints/public/ArtistsAPI.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,23 @@ package com.adamratzman.spotify.endpoints.public
33

44
import com.adamratzman.spotify.main.SpotifyAPI
55
import com.adamratzman.spotify.main.SpotifyRestAction
6+
import com.adamratzman.spotify.main.SpotifyRestActionPaging
67
import com.adamratzman.spotify.utils.Artist
78
import com.adamratzman.spotify.utils.ArtistList
89
import com.adamratzman.spotify.utils.ArtistURI
910
import com.adamratzman.spotify.utils.BadRequestException
1011
import com.adamratzman.spotify.utils.CursorBasedPagingObject
1112
import com.adamratzman.spotify.utils.EndpointBuilder
12-
import com.adamratzman.spotify.utils.LinkedResult
1313
import com.adamratzman.spotify.utils.Market
14+
import com.adamratzman.spotify.utils.PagingObject
1415
import com.adamratzman.spotify.utils.SimpleAlbum
1516
import com.adamratzman.spotify.utils.SpotifyEndpoint
1617
import com.adamratzman.spotify.utils.Track
1718
import com.adamratzman.spotify.utils.catch
1819
import com.adamratzman.spotify.utils.encode
1920
import com.adamratzman.spotify.utils.toInnerArray
20-
import com.adamratzman.spotify.utils.toLinkedResult
2121
import com.adamratzman.spotify.utils.toObject
22+
import com.adamratzman.spotify.utils.toPagingObject
2223
import java.util.function.Supplier
2324

2425
/**
@@ -70,15 +71,15 @@ class ArtistsAPI(api: SpotifyAPI) : SpotifyEndpoint(api) {
7071
offset: Int? = null,
7172
market: Market? = null,
7273
vararg include: AlbumInclusionStrategy
73-
): SpotifyRestAction<LinkedResult<SimpleAlbum>> {
74-
return toAction(Supplier {
74+
): SpotifyRestActionPaging<SimpleAlbum, PagingObject<SimpleAlbum>> {
75+
return toActionPaging(Supplier {
7576
get(
7677
EndpointBuilder("/artists/${ArtistURI(artist).id.encode()}/albums").with("limit", limit).with(
7778
"offset",
7879
offset
7980
).with("market", market?.code)
8081
.with("include_groups", include.joinToString(",") { it.keyword }).toString()
81-
).toLinkedResult<SimpleAlbum>(api)
82+
).toPagingObject<SimpleAlbum>(null, this)
8283
})
8384
}
8485

src/main/kotlin/com/adamratzman/spotify/endpoints/public/BrowseAPI.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ class BrowseAPI(api: SpotifyAPI) : SpotifyEndpoint(api) {
137137
EndpointBuilder("/browse/categories").with("limit", limit).with("offset", offset).with(
138138
"market",
139139
market?.code
140-
)
141-
.with("locale", locale).toString()
140+
).with("locale", locale).toString()
142141
).toPagingObject<SpotifyCategory>(
143142
"categories", endpoint = this
144143
)

src/main/kotlin/com/adamratzman/spotify/endpoints/public/PlaylistsAPI.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.adamratzman.spotify.main.SpotifyRestAction
66
import com.adamratzman.spotify.main.SpotifyRestActionPaging
77
import com.adamratzman.spotify.utils.BadRequestException
88
import com.adamratzman.spotify.utils.EndpointBuilder
9-
import com.adamratzman.spotify.utils.LinkedResult
109
import com.adamratzman.spotify.utils.Market
1110
import com.adamratzman.spotify.utils.PagingObject
1211
import com.adamratzman.spotify.utils.Playlist
@@ -19,7 +18,6 @@ import com.adamratzman.spotify.utils.UserURI
1918
import com.adamratzman.spotify.utils.catch
2019
import com.adamratzman.spotify.utils.encode
2120
import com.adamratzman.spotify.utils.toArray
22-
import com.adamratzman.spotify.utils.toLinkedResult
2321
import com.adamratzman.spotify.utils.toObject
2422
import com.adamratzman.spotify.utils.toPagingObject
2523
import java.util.function.Supplier
@@ -88,13 +86,13 @@ open class PlaylistsAPI(api: SpotifyAPI) : SpotifyEndpoint(api) {
8886
limit: Int? = null,
8987
offset: Int? = null,
9088
market: Market? = null
91-
): SpotifyRestAction<LinkedResult<PlaylistTrack>> {
92-
return toAction(Supplier {
89+
): SpotifyRestActionPaging<PlaylistTrack, PagingObject<PlaylistTrack>> {
90+
return toActionPaging(Supplier {
9391
get(
9492
EndpointBuilder("/playlists/${PlaylistURI(playlist).id.encode()}/tracks").with("limit", limit)
9593
.with("offset", offset).with("market", market?.code).toString()
9694
)
97-
.toLinkedResult<PlaylistTrack>(api)
95+
.toPagingObject<PlaylistTrack>(null, this)
9896
})
9997
}
10098

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ abstract class SpotifyAPI internal constructor(
255255
var token: Token,
256256
var useCache: Boolean
257257
) {
258-
internal var expireTime = System.currentTimeMillis() + token.expires_in * 1000
258+
internal var expireTime = System.currentTimeMillis() + token.expiresIn * 1000
259259
internal val executor = Executors.newScheduledThreadPool(2)
260260

261261
abstract val search: SearchAPI
@@ -313,7 +313,7 @@ class SpotifyAppAPI internal constructor(clientId: String, clientSecret: String,
313313
override fun refreshToken() {
314314
if (clientId != "not-set" && clientSecret != "not-set")
315315
getCredentialedToken(clientId, clientSecret)?.let { token = it }
316-
expireTime = System.currentTimeMillis() + token.expires_in * 1000
316+
expireTime = System.currentTimeMillis() + token.expiresIn * 1000
317317
}
318318

319319
override fun clearCache() = clearAllCaches(
@@ -360,11 +360,11 @@ class SpotifyClientAPI internal constructor(
360360
private fun init(automaticRefresh: Boolean) {
361361
if (automaticRefresh) {
362362
if (clientId != "not-set" && clientSecret != "not-set" && redirectUri != "not-set") {
363-
if (token.expires_in > 60) {
363+
if (token.expiresIn > 60) {
364364
executor.scheduleAtFixedRate(
365365
{ refreshToken() },
366-
(token.expires_in - 30).toLong(),
367-
(token.expires_in - 30).toLong(),
366+
(token.expiresIn - 30).toLong(),
367+
(token.expiresIn - 30).toLong(),
368368
TimeUnit.SECONDS
369369
)
370370
} else {
@@ -385,16 +385,16 @@ class SpotifyClientAPI internal constructor(
385385
HttpConnection(
386386
url = "https://accounts.spotify.com/api/token",
387387
method = HttpRequestMethod.POST,
388-
body = "grant_type=refresh_token&refresh_token=${token.refresh_token ?: ""}",
388+
body = "grant_type=refresh_token&refresh_token=${token.refreshToken ?: ""}",
389389
contentType = "application/x-www-form-urlencoded"
390390
).execute(HttpHeader("Authorization", "Basic ${"$clientId:$clientSecret".byteEncode()}")).body
391391
.toObjectNullable<Token>(null)
392-
if (tempToken?.access_token == null) {
392+
if (tempToken?.accessToken == null) {
393393
logger.logWarning("Spotify token refresh failed")
394394
} else {
395395
this.token = tempToken.copy(
396-
refresh_token = tempToken.refresh_token ?: this.token.refresh_token,
397-
scope = tempToken.scope ?: this.token.scope
396+
refreshToken = tempToken.refreshToken ?: this.token.refreshToken,
397+
scopes = tempToken.scopes ?: this.token.scopes
398398
)
399399
logger.logInfo("Successfully refreshed the Spotify token")
400400
}

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,23 @@ data class Token(
1212
@Json(name = "token_type") val tokenType: String,
1313
@Json(name = "expires_in")val expiresIn: Int,
1414
@Json(name = "refresh_token") val refreshToken: String? = null,
15-
@Json(ignored = false) private val scopeString: String? = null
16-
) {
15+
@Json(ignored = false) private val scopeString: String? = null,
1716
@Json(ignored = true) val scopes: List<SpotifyScope>? = scopeString?.let { str ->
1817
str.split(" ").mapNotNull { scope -> SpotifyScope.values().find { it.uri == scope } }
1918
}
20-
}
19+
)
2120

21+
/**
22+
* Wrapper around [ErrorObject]
23+
*/
2224
data class ErrorResponse(val error: ErrorObject)
2325

26+
/**
27+
* Contains a parsed error from Spotify
28+
*
29+
* @property status The HTTP status code
30+
* @property message A short description of the cause of the error.
31+
*/
2432
data class ErrorObject(val status: Int, val message: String)
2533

2634
class SpotifyUriException(message: String) : BadRequestException(message)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ abstract class SpotifyEndpoint(val api: SpotifyAPI) {
109109
method,
110110
body,
111111
contentType,
112-
HttpHeader("Authorization", "Bearer ${api.token.access_token}")
112+
HttpHeader("Authorization", "Bearer ${api.token.accessToken}")
113113
)
114114

115115
fun <T> toAction(supplier: Supplier<T>) = SpotifyRestAction(api, supplier)

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

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,19 @@ import com.adamratzman.spotify.main.SpotifyException
66
import com.beust.klaxon.Json
77
import com.beust.klaxon.JsonBase
88
import com.beust.klaxon.Klaxon
9-
import java.io.InvalidObjectException
109
import java.net.URLEncoder
1110
import java.util.Base64
1211

12+
/**
13+
* The cursor to use as key to find the next page of items.
14+
*
15+
* @property after nullable cursor value
16+
*/
1317
data class Cursor(val after: String?)
1418

15-
data class LinkedResult<out T>(val href: String, val items: List<T>) {
16-
fun toPlaylist(): PlaylistURI {
17-
if (href.startsWith("https://api.spotify.com/v1/users/")) {
18-
val split = href.removePrefix("https://api.spotify.com/v1/users/").split("/playlists/")
19-
if (split.size == 2) return PlaylistURI(split[1].split("/")[0])
20-
}
21-
throw InvalidObjectException("This object is not linked to a playlist")
22-
}
23-
24-
fun getArtist(): ArtistURI {
25-
if (href.startsWith("https://api.spotify.com/v1/artists/")) {
26-
return ArtistURI(href.removePrefix("https://api.spotify.com/v1/artists/").split("/")[0])
27-
}
28-
throw InvalidObjectException("This object is not linked to an artist")
29-
}
30-
31-
fun getAlbum(): AlbumURI {
32-
if (href.startsWith("https://api.spotify.com/v1/albums/")) {
33-
return AlbumURI(href.removePrefix("https://api.spotify.com/v1/albums/").split("/")[0])
34-
}
35-
throw InvalidObjectException("This object is not linked to an album")
36-
}
37-
}
19+
/**
20+
*
21+
*/
3822

3923
abstract class RelinkingAvailableResponse(@Json(ignored = true) val linkedTrack: LinkedTrack? = null) : Linkable() {
4024
fun isRelinked() = linkedTrack != null
@@ -122,14 +106,6 @@ internal inline fun <reified T> String.toCursorBasedPagingObject(
122106
}
123107
}
124108

125-
internal inline fun <reified T> String.toLinkedResult(api: SpotifyAPI): LinkedResult<T> {
126-
val jsonObject = api.klaxon.parseJsonObject(this.reader())
127-
return LinkedResult(
128-
jsonObject.string("href")!!,
129-
(jsonObject["items"] as JsonBase).toJsonString().toArray(api)
130-
)
131-
}
132-
133109
internal inline fun <reified T> String.toInnerObject(innerName: String, api: SpotifyAPI): T {
134110
val jsonObject = api.klaxon.parseJsonObject(this.reader())
135111

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

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ import java.util.function.Supplier
2929

3030
enum class PagingTraversalType { BACKWARDS, FORWARDS }
3131

32+
/**
33+
* The offset-based paging object is a container for a set of objects. It contains a key called items
34+
* (whose value is an array of the requested objects) along with other keys like previous, next and
35+
* limit that can be useful in future calls.
36+
*
37+
* @property href A link to the Web API endpoint returning the full result of the request.
38+
* @property items The requested data.
39+
* @property limit The maximum number of items in the response (as set in the query or by default).
40+
* @property next URL to the next page of items. ( null if none)
41+
* @property previous URL to the previous page of items. ( null if none)
42+
* @property total The maximum number of items available to return.
43+
* @property offset The offset of the items returned (as set in the query or by default).
44+
*/
3245
class PagingObject<T>(
3346
href: String,
3447
items: List<T>,
@@ -38,12 +51,18 @@ class PagingObject<T>(
3851
previous: String?,
3952
total: Int
4053
) : AbstractPagingObject<T>(href, items, limit, next, offset, previous, total) {
54+
/**
55+
* Get the next set of [T] items
56+
*/
4157
fun getNext() = endpoint.toAction(Supplier {
4258
catch {
4359
getImpl(PagingTraversalType.FORWARDS) as? PagingObject<T>
4460
}
4561
})
4662

63+
/**
64+
* Get the previous set of [T] items
65+
*/
4766
fun getPrevious() = endpoint.toAction(Supplier {
4867
catch {
4968
getImpl(PagingTraversalType.BACKWARDS) as? PagingObject<T>
@@ -52,16 +71,16 @@ class PagingObject<T>(
5271

5372
override fun getImpl(type: PagingTraversalType): AbstractPagingObject<T>? {
5473
return (if (type == PagingTraversalType.FORWARDS) next else previous)?.let { endpoint.get(it) }?.let { json ->
55-
when {
56-
itemClazz == SimpleTrack::class.java -> json.toPagingObject<SimpleTrack>(null, endpoint)
57-
itemClazz == SpotifyCategory::class.java -> json.toPagingObject<SpotifyCategory>(null, endpoint)
58-
itemClazz == SimpleAlbum::class.java -> json.toPagingObject<SimpleAlbum>(null, endpoint)
59-
itemClazz == SimplePlaylist::class.java -> json.toPagingObject<SimplePlaylist>(null, endpoint)
60-
itemClazz == SavedTrack::class.java -> json.toPagingObject<SavedTrack>(null, endpoint)
61-
itemClazz == SavedAlbum::class.java -> json.toPagingObject<SavedAlbum>(null, endpoint)
62-
itemClazz == Artist::class.java -> json.toPagingObject<Artist>(null, endpoint)
63-
itemClazz == Track::class.java -> json.toPagingObject<Track>(null, endpoint)
64-
itemClazz == PlaylistTrack::class.java -> json.toPagingObject<PlaylistTrack>(null, endpoint)
74+
when (itemClazz) {
75+
SimpleTrack::class.java -> json.toPagingObject<SimpleTrack>(null, endpoint)
76+
SpotifyCategory::class.java -> json.toPagingObject<SpotifyCategory>(null, endpoint)
77+
SimpleAlbum::class.java -> json.toPagingObject<SimpleAlbum>(null, endpoint)
78+
SimplePlaylist::class.java -> json.toPagingObject<SimplePlaylist>(null, endpoint)
79+
SavedTrack::class.java -> json.toPagingObject<SavedTrack>(null, endpoint)
80+
SavedAlbum::class.java -> json.toPagingObject<SavedAlbum>(null, endpoint)
81+
Artist::class.java -> json.toPagingObject<Artist>(null, endpoint)
82+
Track::class.java -> json.toPagingObject<Track>(null, endpoint)
83+
PlaylistTrack::class.java -> json.toPagingObject<PlaylistTrack>(null, endpoint)
6584
else -> throw IllegalArgumentException("Unknown type in $href response")
6685
} as? PagingObject<T>
6786
}
@@ -87,28 +106,56 @@ class PagingObject<T>(
87106
return pagingObjects.asSequence()
88107
}
89108

109+
/**
110+
* Get all PagingObjects associated with the request
111+
*/
90112
fun getAll() = endpoint.toAction(Supplier { (getAllImpl() as Sequence<PagingObject<T>>).toList() })
113+
114+
/**
115+
* Get all items of type [T] associated with the request
116+
*/
91117
fun getAllItems() = endpoint.toAction(Supplier { getAll().complete().map { it.items }.flatten() })
92118
}
93119

120+
/**
121+
* The cursor-based paging object is a container for a set of objects. It contains a key called
122+
* items (whose value is an array of the requested objects) along with other keys like next and
123+
* cursors that can be useful in future calls.
124+
*
125+
* @property href A link to the Web API endpoint returning the full result of the request.
126+
* @property items The requested data.
127+
* @property limit The maximum number of items in the response (as set in the query or by default).
128+
* @property next URL to the next page of items. ( null if none)
129+
* @property total The maximum number of items available to return.
130+
* @property cursor The cursors used to find the next set of items..
131+
*/
94132
class CursorBasedPagingObject<T>(
95133
href: String,
96134
items: List<T>,
97135
limit: Int,
98136
next: String?,
99-
val cursors: Cursor,
137+
@Json(name = "cursors") val cursor: Cursor,
100138
total: Int
101139
) : AbstractPagingObject<T>(href, items, limit, next, 0, null, total) {
140+
/**
141+
* Get the next set of [T] items
142+
*/
102143
fun getNext() = endpoint.toAction(Supplier {
103144
catch {
104145
getImpl(PagingTraversalType.FORWARDS) as? CursorBasedPagingObject<T>
105146
}
106147
})
107148

149+
/**
150+
* Get all CursorBasedPagingObjects associated with the request
151+
*/
108152
fun getAll() = endpoint.toAction(Supplier {
109153
getAllImpl() as Sequence<CursorBasedPagingObject<T>>
110154
})
111155

156+
/**
157+
* Get all items of type [T] associated with the request
158+
*/
112159
fun getAllItems() = endpoint.toAction(Supplier {
113160
getAll().complete().map { it.items }.flatten().toList()
114161
})
@@ -138,6 +185,15 @@ class CursorBasedPagingObject<T>(
138185
}
139186
}
140187

188+
/**
189+
* @property href A link to the Web API endpoint returning the full result of the request.
190+
* @property items The requested data.
191+
* @property limit The maximum number of items in the response (as set in the query or by default).
192+
* @property next URL to the next page of items. ( null if none)
193+
* @property previous URL to the previous page of items. ( null if none)
194+
* @property total The maximum number of items available to return.
195+
* @property offset The offset of the items returned (as set in the query or by default).
196+
*/
141197
abstract class AbstractPagingObject<T>(
142198
val href: String,
143199
val items: List<T>,
@@ -148,7 +204,7 @@ abstract class AbstractPagingObject<T>(
148204
val total: Int
149205
) : ArrayList<T>(items) {
150206
@Json(ignored = true)
151-
lateinit var endpoint: SpotifyEndpoint
207+
internal lateinit var endpoint: SpotifyEndpoint
152208

153209
@Json(ignored = true)
154210
lateinit var itemClazz: Class<T>

0 commit comments

Comments
 (0)