Skip to content

Commit 0bbdf08

Browse files
committed
fix cursor-based paging object multi-retrieval, old uris, update samples
1 parent 73188b1 commit 0bbdf08

File tree

14 files changed

+119
-70
lines changed

14 files changed

+119
-70
lines changed

samples/src/main/kotlin/private/ClientFollowingApiSample.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
package private
33

44
import com.adamratzman.spotify.SpotifyApi.Companion.spotifyClientApi
5+
import com.adamratzman.spotify.annotations.SpotifyExperimentalHttpApi
56

7+
@SpotifyExperimentalHttpApi
68
fun main() {
79
// instantiate api
810
val api = spotifyClientApi(
@@ -24,7 +26,7 @@ fun main() {
2426
println(api.following.isFollowingArtist("spotify:artist:7wjeXCtRND2ZdKfMJFu6JC").complete())
2527

2628
// get all followed artists, limiting 1 each request
27-
println(api.following.getFollowedArtists(limit = 1).getAllItems().complete().map { it.name })
29+
println(api.following.getFollowedArtists(limit = 1).getAllItems().complete())
2830

2931
// follow and unfollow, if you weren't previously following, the artist Louane
3032

@@ -37,12 +39,12 @@ fun main() {
3739

3840
// follow and unfollow, if you weren't previously following, the user adamratzman1
3941

40-
val isFollowingAdam = api.following.isFollowingUser("adamratzman1").complete()
42+
val isFollowingAdam = api.following.isFollowingUser("adamratzman").complete()
4143

42-
if (isFollowingAdam) api.following.unfollowUser("adamratzman1").complete()
43-
api.following.followUser("adamratzman1").complete()
44+
if (isFollowingAdam) api.following.unfollowUser("adamratzman").complete()
45+
api.following.followUser("adamratzman").complete()
4446

45-
if (!isFollowingAdam) api.following.unfollowUser("adamratzman1").complete()
47+
if (!isFollowingAdam) api.following.unfollowUser("adamratzman").complete()
4648

4749
// follow and unfollow, if you weren't previously following, the playlist Today's Top Hits
4850

samples/src/main/kotlin/private/ClientPlayerApiSample.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,18 @@ fun main() {
2929
// get the currenty played PlaybackActions
3030
println(api.player.getCurrentlyPlaying().complete()?.actions)
3131

32-
// pause playback on the current device
32+
// play song "I'm Good" by the Mowgli's
33+
api.player.startPlayback(tracksToPlay = listOf(api.search.searchTrack("I'm Good the Mowgli's").complete()[0].uri.uri)).complete()
34+
35+
// pause playback on the current device.
3336
api.player.pause().complete()
3437

3538
// seek the beginning of the track currently playing
3639
api.player.seek(0).complete()
3740

41+
// resume playback
42+
api.player.resume().complete()
43+
3844
// set repeat the current track
3945
api.player.setRepeatMode(TRACK).complete()
4046

@@ -47,15 +53,9 @@ fun main() {
4753
// skip back to the last track that was in the user queue
4854
api.player.skipBehind().complete()
4955

50-
// resume playback
51-
api.player.resume().complete()
52-
53-
// play song "I'm Good" by the Mowgli's
54-
api.player.startPlayback(tracksToPlay = listOf(api.search.searchTrack("I'm Good the Mowgli's").complete()[0].uri.uri)).complete()
55-
5656
// toggle shuffling
5757
api.player.toggleShuffle(shuffle = true).complete()
5858

5959
// transfer playback
60-
api.player.transferPlayback("your_device_id").complete()
60+
api.player.transferPlayback(api.player.getDevices().complete().first().id!!).complete()
6161
}

samples/src/main/kotlin/private/ClientPlaylistApiSample.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ fun main() {
3939
// for an example of re-ordering tracks
4040

4141
// replace the tracks in a client playlist with two songs by Lorde
42-
api.playlists.setClientPlaylistTracks(playlist.id, "spotify:track:6ie2Bw3xLj2JcGowOlcMhb", "").complete()
42+
api.playlists.setClientPlaylistTracks(playlist.id, "spotify:track:6ie2Bw3xLj2JcGowOlcMhb", "spotify:track:2dLLR6qlu5UJ5gk0dKz0h3").complete()
4343

4444
// remove the song we just added from our client playlist
4545
api.playlists.removeTrackFromClientPlaylist(playlist.id, "spotify:track:6ie2Bw3xLj2JcGowOlcMhb").complete()

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,29 @@ sealed class SpotifyException(message: String, cause: Throwable? = null) : Excep
1111
/**
1212
* Thrown when a request fails
1313
*/
14-
open class BadRequestException(message: String, val statusCode: Int? = null, cause: Throwable? = null) :
15-
SpotifyException(message, cause) {
16-
constructor(message: String, cause: Throwable? = null) : this(message, null, cause)
14+
open class BadRequestException(message: String, val statusCode: Int? = null, val reason: String? = null, cause: Throwable? = null) :
15+
SpotifyException(message, cause) {
16+
constructor(message: String, cause: Throwable? = null) : this(message, null, null, cause)
1717
constructor(error: ErrorObject, cause: Throwable? = null) : this(
18-
"Received Status Code ${error.status}. Error cause: ${error.message}",
19-
error.status,
20-
cause
18+
"Received Status Code ${error.status}. Error cause: ${error.message}" + (error.reason?.let { ". Reason: ${error.reason}" }
19+
?: ""),
20+
error.status,
21+
error.reason,
22+
cause
2123
)
2224

2325
constructor(authenticationError: AuthenticationError) :
2426
this(
25-
"Authentication error: ${authenticationError.error}. Description: ${authenticationError.description}",
26-
401
27+
"Authentication error: ${authenticationError.error}. Description: ${authenticationError.description}",
28+
401
2729
)
2830

2931
constructor(responseException: ResponseException) :
3032
this(
31-
responseException.message ?: "Bad Request",
32-
responseException.response.status.value,
33-
responseException
33+
responseException.message ?: "Bad Request",
34+
responseException.response.status.value,
35+
null,
36+
responseException
3437
)
3538
}
3639

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package com.adamratzman.spotify.http
33

44
import com.adamratzman.spotify.SpotifyApi
55
import com.adamratzman.spotify.SpotifyException
6+
import com.adamratzman.spotify.models.ErrorResponse
67
import com.adamratzman.spotify.models.SpotifyRatelimitedException
8+
import com.adamratzman.spotify.models.serialization.stableJson
9+
import com.adamratzman.spotify.models.serialization.toObject
710
import com.adamratzman.spotify.utils.getCurrentTimeMs
811
import io.ktor.client.HttpClient
912
import io.ktor.client.features.ResponseException
@@ -144,7 +147,9 @@ class HttpConnection constructor(
144147
} catch (e: CancellationException) {
145148
throw e
146149
} catch (e: ResponseException) {
147-
throw SpotifyException.BadRequestException(e)
150+
val errorBody = e.response.readText()
151+
val error = errorBody.toObject(ErrorResponse.serializer(), api, api?.json ?: stableJson).error
152+
throw SpotifyException.BadRequestException(error)
148153
}
149154
}
150155

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ class PagingObject<T : Any>(
7676
val endpointFinal = endpoint!!
7777
return (if (type == FORWARDS) next else previous)?.let { endpoint!!.get(it) }?.let { json ->
7878
when (itemClazz) {
79-
SimpleTrack::class -> json.toPagingObject(SimpleTrack.serializer(), null, endpointFinal, endpointFinal.api.json)
80-
SpotifyCategory::class -> json.toPagingObject(SpotifyCategory.serializer(), "categories", endpointFinal, endpointFinal.api.json)
81-
SimpleAlbum::class -> json.toPagingObject(SimpleAlbum.serializer(), "albums", endpointFinal, endpointFinal.api.json)
82-
SimplePlaylist::class -> json.toPagingObject(SimplePlaylist.serializer(), "playlists", endpointFinal, endpointFinal.api.json)
83-
SavedTrack::class -> json.toPagingObject(SavedTrack.serializer(), null, endpointFinal, endpointFinal.api.json)
84-
SavedAlbum::class -> json.toPagingObject(SavedAlbum.serializer(), null, endpointFinal, endpointFinal.api.json)
85-
Artist::class -> json.toPagingObject(Artist.serializer(), null, endpointFinal, endpointFinal.api.json)
86-
Track::class -> json.toPagingObject(Track.serializer(), null, endpointFinal, endpointFinal.api.json)
87-
PlaylistTrack::class -> json.toPagingObject(PlaylistTrack.serializer(), null, endpointFinal, endpointFinal.api.json)
79+
SimpleTrack::class -> json.toPagingObject(SimpleTrack.serializer(), null, endpointFinal, endpointFinal.api.json, true)
80+
SpotifyCategory::class -> json.toPagingObject(SpotifyCategory.serializer(), "categories", endpointFinal, endpointFinal.api.json, true)
81+
SimpleAlbum::class -> json.toPagingObject(SimpleAlbum.serializer(), "albums", endpointFinal, endpointFinal.api.json, true)
82+
SimplePlaylist::class -> json.toPagingObject(SimplePlaylist.serializer(), "playlists", endpointFinal, endpointFinal.api.json, true)
83+
SavedTrack::class -> json.toPagingObject(SavedTrack.serializer(), null, endpointFinal, endpointFinal.api.json, true)
84+
SavedAlbum::class -> json.toPagingObject(SavedAlbum.serializer(), null, endpointFinal, endpointFinal.api.json, true)
85+
Artist::class -> json.toPagingObject(Artist.serializer(), null, endpointFinal, endpointFinal.api.json, true)
86+
Track::class -> json.toPagingObject(Track.serializer(), null, endpointFinal, endpointFinal.api.json, true)
87+
PlaylistTrack::class -> json.toPagingObject(PlaylistTrack.serializer(), null, endpointFinal, endpointFinal.api.json, true)
8888
else -> throw IllegalArgumentException("Unknown type in $href response")
8989
} as? PagingObject<T>
9090
}

src/commonMain/kotlin/com.adamratzman.spotify/models/Player.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ data class Device(
5454
@SerialName("is_private_session") val isPrivateSession: Boolean,
5555
@SerialName("is_restricted") val isRestricted: Boolean,
5656
val name: String,
57-
val typeString: String,
57+
@SerialName("type") val typeString: String,
5858
@SerialName("volume_percent") val volumePercent: Int
5959
) : IdentifiableNullable() {
6060
@Transient
@@ -106,7 +106,7 @@ data class CurrentlyPlayingContext(
106106
@SerialName("item") val track: Track? = null,
107107
@SerialName("shuffle_state") val shuffleState: Boolean,
108108
@SerialName("repeat_state") val repeatStateString: String,
109-
val context: Context
109+
val context: Context? = null
110110
) {
111111
@Transient
112112
val repeatState: RepeatState = RepeatState.values().match(repeatStateString)!!

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ data class ErrorResponse(val error: ErrorObject, @Transient val exception: Excep
109109
* @property message A short description of the cause of the error.
110110
*/
111111
@Serializable
112-
data class ErrorObject(val status: Int, val message: String)
112+
data class ErrorObject(val status: Int, val message: String, val reason: String? = null)
113113

114114
/**
115115
* An exception during the authentication process

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

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ private class SimpleUriSerializer<T : SpotifyUri>(val ctor: (String) -> T) : KSe
4141
override fun serialize(encoder: Encoder, obj: T) = encoder.encodeString(obj.uri)
4242
}
4343

44+
@PublishedApi
45+
internal fun fixSpotifyUri(uri: String): String {
46+
val matchesOldPlaylistUriFormat = "^spotify:user:(?:.*:)*playlist:(.+)\$".toRegex().matchEntire(uri)
47+
return (matchesOldPlaylistUriFormat?.let { result -> "spotify:playlist:${result.groupValues[1]}" } ?: uri)
48+
.replace(" ", "")
49+
}
50+
4451
/**
4552
* Represents a Spotify URI, parsed from either a Spotify ID or taken from an endpoint.
4653
*
@@ -53,7 +60,7 @@ sealed class SpotifyUri(input: String, type: String) {
5360
val id: String
5461

5562
init {
56-
input.replace(" ", "").let {
63+
fixSpotifyUri(input).let {
5764
this.uri = it.add(type)
5865
this.id = it.remove(type)
5966
}
@@ -71,7 +78,7 @@ sealed class SpotifyUri(input: String, type: String) {
7178
}
7279

7380
override fun toString(): String {
74-
return "SpotifyUri($uri)"
81+
return "SpotifyUri(${fixSpotifyUri(uri)})"
7582
}
7683

7784
@Serializer(forClass = SpotifyUri::class)
@@ -85,7 +92,7 @@ sealed class SpotifyUri(input: String, type: String) {
8592
* */
8693
inline fun <T : SpotifyUri> safeInitiate(uri: String, ctor: (String) -> T): T? {
8794
return try {
88-
ctor(uri)
95+
ctor(fixSpotifyUri(uri))
8996
} catch (e: SpotifyUriException) {
9097
null
9198
}
@@ -94,7 +101,8 @@ sealed class SpotifyUri(input: String, type: String) {
94101
/**
95102
* Creates a abstract SpotifyUri of given input. Doesn't allow ambiguity by disallowing creation by id.
96103
* */
97-
operator fun invoke(input: String): SpotifyUri {
104+
operator fun invoke(inputTemp: String): SpotifyUri {
105+
val input = fixSpotifyUri(inputTemp)
98106
val constructors = listOf(::AlbumUri, ::ArtistUri, TrackUri.Companion::invoke, ::UserUri, ::PlaylistUri)
99107
for (ctor in constructors) {
100108
safeInitiate(input, ctor)?.takeIf { it.uri == input }?.also { return it }
@@ -112,7 +120,8 @@ sealed class SpotifyUri(input: String, type: String) {
112120
* SpotifyUri.isType<UserUri>("spotify:track:abc") // returns: false
113121
* ```
114122
* */
115-
inline fun <reified T : SpotifyUri> isType(input: String): Boolean {
123+
inline fun <reified T : SpotifyUri> isType(inputTemp: String): Boolean {
124+
val input = fixSpotifyUri(inputTemp)
116125
return safeInitiate(input, ::invoke)?.let { it is T } ?: false
117126
}
118127

@@ -125,7 +134,8 @@ sealed class SpotifyUri(input: String, type: String) {
125134
* SpotifyUri.canBeType<UserUri>("spotify:track:abc") // returns: false
126135
* ```
127136
* */
128-
inline fun <reified T : SpotifyUri> canBeType(input: String): Boolean {
137+
inline fun <reified T : SpotifyUri> canBeType(inputTemp: String): Boolean {
138+
val input = fixSpotifyUri(inputTemp)
129139
return isType<T>(input) || !input.contains(':')
130140
}
131141
}

src/commonMain/kotlin/com.adamratzman.spotify/models/serialization/SerializationUtils.kt

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@ internal fun <T : Any> String.toPagingObject(
5959
innerObjectName: String? = null,
6060
endpoint: SpotifyEndpoint,
6161
json: Json,
62-
arbitraryInnerNameAllowed: Boolean = false
62+
arbitraryInnerNameAllowed: Boolean = false,
63+
skipInnerNameFirstIfPossible: Boolean = true
6364
): PagingObject<T> {
64-
if (innerObjectName != null || arbitraryInnerNameAllowed) {
65+
if (innerObjectName != null || (arbitraryInnerNameAllowed && !skipInnerNameFirstIfPossible)) {
6566
val map = this.parseJson { json.parse((String.serializer() to PagingObject.serializer(tSerializer)).map, this) }
6667
return (map[innerObjectName] ?: if (arbitraryInnerNameAllowed) map.keys.firstOrNull()?.let { map[it] }
6768
?: error("") else error(""))
@@ -94,7 +95,8 @@ internal fun <T : Any> String.toPagingObject(
9495
innerObjectName,
9596
endpoint,
9697
json,
97-
true
98+
true,
99+
false
98100
)
99101
} else throw jde
100102
}
@@ -105,41 +107,68 @@ internal inline fun <reified T : Any> String.toPagingObject(
105107
innerObjectName: String? = null,
106108
endpoint: SpotifyEndpoint,
107109
json: Json,
108-
arbitraryInnerNameAllowed: Boolean = false
109-
): PagingObject<T> = toPagingObject(T::class, tSerializer, innerObjectName, endpoint, json, arbitraryInnerNameAllowed)
110+
arbitraryInnerNameAllowed: Boolean = false,
111+
skipInnerNameFirstIfPossible: Boolean = true
112+
): PagingObject<T> = toPagingObject(T::class, tSerializer, innerObjectName, endpoint, json, arbitraryInnerNameAllowed, skipInnerNameFirstIfPossible)
110113

111-
internal inline fun <reified T : Any> String.toCursorBasedPagingObject(
114+
internal fun <T : Any> String.toCursorBasedPagingObject(
115+
tClazz: KClass<T>,
112116
tSerializer: KSerializer<T>,
113117
innerObjectName: String? = null,
114118
endpoint: SpotifyEndpoint,
115-
json: Json
119+
json: Json,
120+
arbitraryInnerNameAllowed: Boolean = false,
121+
skipInnerNameFirstIfPossible: Boolean = true
116122
): CursorBasedPagingObject<T> {
117-
if (innerObjectName != null) {
123+
if (innerObjectName != null || (arbitraryInnerNameAllowed && !skipInnerNameFirstIfPossible)) {
118124
val map = this.parseJson { json.parse((String.serializer() to CursorBasedPagingObject.serializer(tSerializer)).map, this) }
119-
120-
return (map[innerObjectName] ?: error(""))
125+
return (map[innerObjectName] ?: if (arbitraryInnerNameAllowed) map.keys.firstOrNull()?.let { map[it] }
126+
?: error("") else error(""))
121127
.apply {
122128
this.endpoint = endpoint
123-
this.itemClazz = T::class
129+
this.itemClazz = tClazz
124130
this.items.map { obj ->
125131
if (obj is NeedsApi) obj.api = endpoint.api
126132
if (obj is AbstractPagingObject<*>) obj.endpoint = endpoint
127133
}
128134
}
129135
}
136+
return try {
137+
val pagingObject = this.parseJson { json.parse(CursorBasedPagingObject.serializer(tSerializer), this) }
130138

131-
val pagingObject = this.parseJson { json.parse(CursorBasedPagingObject.serializer(tSerializer), this) }
132-
133-
return pagingObject.apply {
134-
this.endpoint = endpoint
135-
this.itemClazz = T::class
136-
this.items.map { obj ->
137-
if (obj is NeedsApi) obj.api = endpoint.api
138-
if (obj is AbstractPagingObject<*>) obj.endpoint = endpoint
139+
pagingObject.apply {
140+
this.endpoint = endpoint
141+
this.itemClazz = tClazz
142+
this.items.map { obj ->
143+
if (obj is NeedsApi) obj.api = endpoint.api
144+
if (obj is AbstractPagingObject<*>) obj.endpoint = endpoint
145+
}
139146
}
147+
} catch (jde: SpotifyException.ParseException) {
148+
if (!arbitraryInnerNameAllowed && jde.message?.contains("unable to parse", true) == true) {
149+
toCursorBasedPagingObject(
150+
tClazz,
151+
tSerializer,
152+
innerObjectName,
153+
endpoint,
154+
json,
155+
arbitraryInnerNameAllowed = true,
156+
skipInnerNameFirstIfPossible = false
157+
)
158+
} else throw jde
140159
}
141160
}
142161

162+
internal inline fun <reified T : Any> String.toCursorBasedPagingObject(
163+
tSerializer: KSerializer<T>,
164+
innerObjectName: String? = null,
165+
endpoint: SpotifyEndpoint,
166+
json: Json,
167+
arbitraryInnerNameAllowed: Boolean = false,
168+
skipInnerNameFirstIfPossible: Boolean = true
169+
): CursorBasedPagingObject<T> =
170+
toCursorBasedPagingObject(T::class, tSerializer, innerObjectName, endpoint, json, arbitraryInnerNameAllowed, skipInnerNameFirstIfPossible)
171+
143172
internal inline fun <reified T> String.toInnerObject(serializer: KSerializer<T>, innerName: String, json: Json): T {
144173
val map = this.parseJson { json.parse((String.serializer() to serializer).map, this) }
145174
return (map[innerName] ?: error("Inner object with name $innerName doesn't exist in $map"))

0 commit comments

Comments
 (0)