From 66ff222e4937a9097668adec9e4019f6b88de2a9 Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 15:26:32 +0100 Subject: [PATCH 01/10] Add Premium subscription note to README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index eb1f4772..0a293a5f 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,8 @@ to the sections below or the [Spotify authorization guide](https://developer.spo **Note**: You can use the online [Spotify OAuth Token Generator](https://adamratzman.com/projects/spotify/generate-token) tool to generate a client token for local testing. +**Important**: As of March 2026, to use any API endpoints, you must have a Spotify Premium subscription, unless you are in extended quota mode. + ### SpotifyAppApi This provides access only to public Spotify endpoints. Use this when you have a server-side application. Note that implicit grant authorization From 4df799377e5d69f097da610e149e1f78118ac747 Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 15:27:10 +0100 Subject: [PATCH 02/10] Fix commonJvmLikeTest dependencies --- build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 06dceb5f..ebb2762e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -210,6 +210,8 @@ kotlin { } val commonJvmLikeTest by creating { + dependsOn(commonTest.get()) + dependencies { implementation(kotlin("test-junit")) implementation("com.sparkjava:spark-core:$sparkVersion") From eedbdc0251421a05336feec63396becf0a5eae5b Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 15:27:24 +0100 Subject: [PATCH 03/10] Mark relevant endpoints as @SpotifyExtendedQuota --- .../annotations/RestrictionAnnotations.kt | 13 +++++++++++++ .../endpoints/client/ClientEpisodeApi.kt | 2 ++ .../endpoints/pub/AlbumApi.kt | 2 ++ .../endpoints/pub/ArtistApi.kt | 4 ++++ .../endpoints/pub/BrowseApi.kt | 9 +++++++++ .../endpoints/pub/EpisodeApi.kt | 2 ++ .../endpoints/pub/MarketsApi.kt | 2 ++ .../endpoints/pub/PlaylistApi.kt | 2 ++ .../endpoints/pub/ShowApi.kt | 9 +++------ .../endpoints/pub/TrackApi.kt | 5 +++++ .../endpoints/pub/UserApi.kt | 2 ++ .../spotify/priv/ClientEpisodeApiTest.kt | 15 ++++++--------- .../com.adamratzman/spotify/pub/BrowseApiTest.kt | 3 +++ .../com.adamratzman/spotify/pub/EpisodeApiTest.kt | 4 ++-- .../spotify/pub/PublicArtistsApiTest.kt | 2 +- .../spotify/pub/PublicTracksApiTest.kt | 2 ++ .../spotify/utilities/UtilityTests.kt | 10 ++++++---- 17 files changed, 66 insertions(+), 22 deletions(-) create mode 100644 src/commonMain/kotlin/com.adamratzman.spotify/annotations/RestrictionAnnotations.kt diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/annotations/RestrictionAnnotations.kt b/src/commonMain/kotlin/com.adamratzman.spotify/annotations/RestrictionAnnotations.kt new file mode 100644 index 00000000..b8383bcd --- /dev/null +++ b/src/commonMain/kotlin/com.adamratzman.spotify/annotations/RestrictionAnnotations.kt @@ -0,0 +1,13 @@ +package com.adamratzman.spotify.annotations + +/** + * The underlying endpoint or field is only available in Extended Quota Mode after March 9, 2026. + * It is **not available** in development mode! + */ +@RequiresOptIn( + level = RequiresOptIn.Level.WARNING, + message = "Requires Extended Quota Mode after March 9, 2026" +) +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +public annotation class SpotifyExtendedQuota \ No newline at end of file diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientEpisodeApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientEpisodeApi.kt index b9507ca0..bdb2ebda 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientEpisodeApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientEpisodeApi.kt @@ -4,6 +4,7 @@ package com.adamratzman.spotify.endpoints.client import com.adamratzman.spotify.GenericSpotifyApi import com.adamratzman.spotify.SpotifyException.BadRequestException import com.adamratzman.spotify.SpotifyScope +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.endpoints.pub.EpisodeApi import com.adamratzman.spotify.models.Episode import com.adamratzman.spotify.models.EpisodeList @@ -54,6 +55,7 @@ public class ClientEpisodeApi(api: GenericSpotifyApi) : EpisodeApi(api) { * @return List of possibly-null [Episode] objects. * @throws BadRequestException If any invalid show id is provided */ + @SpotifyExtendedQuota public suspend fun getEpisodes(vararg ids: String): List { requireScopes(SpotifyScope.UserReadPlaybackPosition) checkBulkRequesting(50, ids.size) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/AlbumApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/AlbumApi.kt index ef543702..e7e4f9c9 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/AlbumApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/AlbumApi.kt @@ -3,6 +3,7 @@ package com.adamratzman.spotify.endpoints.pub import com.adamratzman.spotify.GenericSpotifyApi import com.adamratzman.spotify.SpotifyException.BadRequestException +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.http.SpotifyEndpoint import com.adamratzman.spotify.models.Album import com.adamratzman.spotify.models.AlbumUri @@ -52,6 +53,7 @@ public class AlbumApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @return List of [Album] objects or null if the album could not be found, in the order requested */ + @SpotifyExtendedQuota public suspend fun getAlbums(vararg albums: String, market: Market? = null): List { checkBulkRequesting(20, albums.size) return bulkStatelessRequest(20, albums.toList()) { chunk -> diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ArtistApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ArtistApi.kt index b1f163b3..6bbdc4be 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ArtistApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ArtistApi.kt @@ -3,6 +3,7 @@ package com.adamratzman.spotify.endpoints.pub import com.adamratzman.spotify.GenericSpotifyApi import com.adamratzman.spotify.SpotifyException.BadRequestException +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.http.SpotifyEndpoint import com.adamratzman.spotify.models.* import com.adamratzman.spotify.models.serialization.toInnerArray @@ -47,6 +48,7 @@ public class ArtistApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @return List of [Artist] objects or null if the artist could not be found, in the order requested. * @throws BadRequestException if any of the [artists] are not found, *if using client api* */ + @SpotifyExtendedQuota public suspend fun getArtists(vararg artists: String): List { checkBulkRequesting(50, artists.size) @@ -116,6 +118,7 @@ public class ArtistApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @throws BadRequestException if tracks are not available in the specified [Market] or the [artist] is not found * @return List of the top [Track]s of an artist in the given market */ + @SpotifyExtendedQuota public suspend fun getArtistTopTracks(artist: String, market: Market = Market.US): List = get( endpointBuilder("/artists/${ArtistUri(artist).id.encodeUrl()}/top-tracks").with( "country", @@ -134,6 +137,7 @@ public class ArtistApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @throws BadRequestException if the [artist] is not found * @return List of *never-null*, but possibly empty [Artist]s representing similar artists */ + @SpotifyExtendedQuota public suspend fun getRelatedArtists(artist: String): List = get(endpointBuilder("/artists/${ArtistUri(artist).id.encodeUrl()}/related-artists").toString()) .toObject(ArtistList.serializer(), api, json).artists.filterNotNull() diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/BrowseApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/BrowseApi.kt index 352c3473..5b670a09 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/BrowseApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/BrowseApi.kt @@ -3,6 +3,7 @@ package com.adamratzman.spotify.endpoints.pub import com.adamratzman.spotify.GenericSpotifyApi import com.adamratzman.spotify.SpotifyException.BadRequestException +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.http.SpotifyEndpoint import com.adamratzman.spotify.models.ArtistUri import com.adamratzman.spotify.models.ErrorObject @@ -39,6 +40,7 @@ public class BrowseApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @return List of genre ids */ + @SpotifyExtendedQuota public suspend fun getAvailableGenreSeeds(): List = get(endpointBuilder("/recommendations/available-genre-seeds").toString()).toInnerArray( ListSerializer(String.serializer()), @@ -59,6 +61,7 @@ public class BrowseApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @throws BadRequestException if filter parameters are illegal * @return [PagingObject] of new album released, ordered by release date (descending) */ + @SpotifyExtendedQuota public suspend fun getNewReleases( limit: Int? = api.spotifyApiOptions.defaultLimit, offset: Int? = null, @@ -88,6 +91,7 @@ public class BrowseApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @throws BadRequestException if filter parameters are illegal or [locale] does not exist * @return [FeaturedPlaylists] object with the current featured message and featured playlists */ + @SpotifyExtendedQuota public suspend fun getFeaturedPlaylists( limit: Int? = api.spotifyApiOptions.defaultLimit, offset: Int? = null, @@ -118,6 +122,7 @@ public class BrowseApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @return Default category list if [locale] is invalid, otherwise the localized PagingObject */ + @SpotifyExtendedQuota public suspend fun getCategoryList( limit: Int? = api.spotifyApiOptions.defaultLimit, offset: Int? = null, @@ -145,6 +150,7 @@ public class BrowseApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @throws BadRequestException if [categoryId] is not found or [locale] does not exist on Spotify */ + @SpotifyExtendedQuota public suspend fun getCategory( categoryId: String, market: Market? = null, @@ -167,6 +173,7 @@ public class BrowseApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @throws BadRequestException if [categoryId] is not found or filters are illegal * @return [PagingObject] of top playlists tagged with [categoryId] */ + @SpotifyExtendedQuota public suspend fun getPlaylistsForCategory( categoryId: String, limit: Int? = api.spotifyApiOptions.defaultLimit, @@ -211,6 +218,7 @@ public class BrowseApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @throws BadRequestException if any filter is applied illegally */ + @SpotifyExtendedQuota public suspend fun getTrackRecommendations( seedArtists: List? = null, seedGenres: List? = null, @@ -265,6 +273,7 @@ public class BrowseApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @throws BadRequestException if any filter is applied illegally * */ + @SpotifyExtendedQuota public suspend fun getRecommendations( seedArtists: List? = null, seedGenres: List? = null, diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/EpisodeApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/EpisodeApi.kt index e7a5e51e..cbb49af0 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/EpisodeApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/EpisodeApi.kt @@ -6,6 +6,7 @@ import com.adamratzman.spotify.SpotifyAppApi import com.adamratzman.spotify.SpotifyClientApi import com.adamratzman.spotify.SpotifyException.BadRequestException import com.adamratzman.spotify.SpotifyScope +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.http.SpotifyEndpoint import com.adamratzman.spotify.models.Episode import com.adamratzman.spotify.models.EpisodeList @@ -63,6 +64,7 @@ public open class EpisodeApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @return List of possibly-null [Episode] objects. * @throws BadRequestException If any invalid show id is provided, if this is a [SpotifyClientApi] */ + @SpotifyExtendedQuota public suspend fun getEpisodes(vararg ids: String, market: Market): List { checkBulkRequesting(50, ids.size) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/MarketsApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/MarketsApi.kt index 500b3072..3961fdd7 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/MarketsApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/MarketsApi.kt @@ -2,6 +2,7 @@ package com.adamratzman.spotify.endpoints.pub import com.adamratzman.spotify.GenericSpotifyApi +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.http.SpotifyEndpoint import com.adamratzman.spotify.models.serialization.toInnerArray import com.adamratzman.spotify.utils.Market @@ -16,6 +17,7 @@ public class MarketsApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @return List of [Market] */ + @SpotifyExtendedQuota public suspend fun getAvailableMarkets(): List { return get(endpointBuilder("/markets").toString()).toInnerArray( ListSerializer(String.serializer()), diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/PlaylistApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/PlaylistApi.kt index 60703d4b..f78ab32d 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/PlaylistApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/PlaylistApi.kt @@ -5,6 +5,7 @@ import com.adamratzman.spotify.GenericSpotifyApi import com.adamratzman.spotify.SpotifyAppApi import com.adamratzman.spotify.SpotifyException.BadRequestException import com.adamratzman.spotify.SpotifyScope +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.http.SpotifyEndpoint import com.adamratzman.spotify.models.PagingObject import com.adamratzman.spotify.models.Playlist @@ -50,6 +51,7 @@ public open class PlaylistApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @throws BadRequestException if the user is not found (404) * */ + @SpotifyExtendedQuota public suspend fun getUserPlaylists( user: String, limit: Int? = api.spotifyApiOptions.defaultLimit, diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ShowApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ShowApi.kt index 4592210e..6cd34ddf 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ShowApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/ShowApi.kt @@ -5,13 +5,9 @@ import com.adamratzman.spotify.GenericSpotifyApi import com.adamratzman.spotify.SpotifyAppApi import com.adamratzman.spotify.SpotifyException.BadRequestException import com.adamratzman.spotify.SpotifyScope +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.http.SpotifyEndpoint -import com.adamratzman.spotify.models.PagingObject -import com.adamratzman.spotify.models.Show -import com.adamratzman.spotify.models.ShowList -import com.adamratzman.spotify.models.ShowUri -import com.adamratzman.spotify.models.SimpleEpisode -import com.adamratzman.spotify.models.SimpleShow +import com.adamratzman.spotify.models.* import com.adamratzman.spotify.models.serialization.toNonNullablePagingObject import com.adamratzman.spotify.models.serialization.toObject import com.adamratzman.spotify.utils.Market @@ -65,6 +61,7 @@ public open class ShowApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @return List of possibly-null [SimpleShow] objects, if the show was not found or invalid ids were provided. */ + @SpotifyExtendedQuota public suspend fun getShows(vararg ids: String, market: Market): List { checkBulkRequesting(50, ids.size) return bulkStatelessRequest(50, ids.toList()) { chunk -> diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/TrackApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/TrackApi.kt index d849091d..ea6af453 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/TrackApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/TrackApi.kt @@ -3,6 +3,7 @@ package com.adamratzman.spotify.endpoints.pub import com.adamratzman.spotify.GenericSpotifyApi import com.adamratzman.spotify.SpotifyException.BadRequestException +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.http.SpotifyEndpoint import com.adamratzman.spotify.models.AudioAnalysis import com.adamratzman.spotify.models.AudioFeatures @@ -51,6 +52,7 @@ public class TrackApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @return List of possibly-null full [Track] objects. */ + @SpotifyExtendedQuota public suspend fun getTracks(vararg tracks: String, market: Market? = null): List { checkBulkRequesting(50, tracks.size) return bulkStatelessRequest(50, tracks.toList()) { chunk -> @@ -79,6 +81,7 @@ public class TrackApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @throws BadRequestException if [track] cannot be found */ + @SpotifyExtendedQuota public suspend fun getAudioAnalysis(track: String): AudioAnalysis = get(endpointBuilder("/audio-analysis/${PlayableUri(track).id.encodeUrl()}").toString()) .toObject(AudioAnalysis.serializer(), api, json) @@ -92,6 +95,7 @@ public class TrackApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @throws BadRequestException if [track] cannot be found */ + @SpotifyExtendedQuota public suspend fun getAudioFeatures(track: String): AudioFeatures = get(endpointBuilder("/audio-features/${PlayableUri(track).id.encodeUrl()}").toString()) .toObject(AudioFeatures.serializer(), api, json) @@ -105,6 +109,7 @@ public class TrackApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @return Ordered list of possibly-null [AudioFeatures] objects. */ + @SpotifyExtendedQuota public suspend fun getAudioFeatures(vararg tracks: String): List { checkBulkRequesting(100, tracks.size) return bulkStatelessRequest(100, tracks.toList()) { chunk -> diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/UserApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/UserApi.kt index a27968f7..719dbe4c 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/UserApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/UserApi.kt @@ -2,6 +2,7 @@ package com.adamratzman.spotify.endpoints.pub import com.adamratzman.spotify.GenericSpotifyApi +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.http.SpotifyEndpoint import com.adamratzman.spotify.models.SpotifyPublicUser import com.adamratzman.spotify.models.UserUri @@ -24,6 +25,7 @@ public open class UserApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @return All publicly-available information about the user */ + @SpotifyExtendedQuota public suspend fun getProfile(user: String): SpotifyPublicUser? = catch(/* some incorrect user ids will return 500 */ catchInternalServerError = true) { get(endpointBuilder("/users/${UserUri(user).id.encodeUrl()}").toString()) .toObject(SpotifyPublicUser.serializer(), api, json) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientEpisodeApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientEpisodeApiTest.kt index ab91ad99..4b44d709 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientEpisodeApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientEpisodeApiTest.kt @@ -9,11 +9,7 @@ import com.adamratzman.spotify.SpotifyException.BadRequestException import com.adamratzman.spotify.runTestOnDefaultDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotNull -import kotlin.test.assertNull +import kotlin.test.* class ClientEpisodeApiTest : AbstractTest() { @Test @@ -22,20 +18,21 @@ class ClientEpisodeApiTest : AbstractTest() { if (!isApiInitialized()) return@runTestOnDefaultDispatcher assertNull(api.episodes.getEpisode("nonexistant episode")) - assertNotNull(api.episodes.getEpisode("3lMZTE81Pbrp0U12WZe27l")) + assertNotNull(api.episodes.getEpisode("4fvIbnHhHaD8xljXI0uRXr")) } + @Ignore // requires extended quota mode @Test fun testGetEpisodes(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetEpisodes.name) if (!isApiInitialized()) return@runTestOnDefaultDispatcher assertFailsWith { api.episodes.getEpisodes("hi", "dad") } - assertFailsWith { api.episodes.getEpisodes("1cfOhXP4GQCd5ZFHoSF8gg", "j")[1] } + assertFailsWith { api.episodes.getEpisodes("4fvIbnHhHaD8xljXI0uRXr", "j")[1] } assertEquals( - listOf("The Great Inflation (Classic)"), - api.episodes.getEpisodes("3lMZTE81Pbrp0U12WZe27l").map { it?.name } + listOf("The Midterms Begin With a Texas-Size Showdown"), + api.episodes.getEpisodes("4fvIbnHhHaD8xljXI0uRXr").map { it?.name } ) } } diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/BrowseApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/BrowseApiTest.kt index ec14ddbd..c33b3f6f 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/BrowseApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/BrowseApiTest.kt @@ -22,6 +22,7 @@ import kotlin.test.assertNotSame import kotlin.test.assertTrue class BrowseApiTest : AbstractTest() { + @Ignore // requires extended quota mode @Test fun testGenreSeeds(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGenreSeeds.name) @@ -72,6 +73,7 @@ class BrowseApiTest : AbstractTest() { ) } + @Ignore // requires extended quota mode @Test fun testGetFeaturedPlaylists(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetFeaturedPlaylists.name) @@ -96,6 +98,7 @@ class BrowseApiTest : AbstractTest() { assertTrue(api.browse.getNewReleases(limit = 6, offset = 44, market = Market.US).items.isNotEmpty()) } + @Ignore // requires extended quota mode @Test fun testGetRecommendations(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetRecommendations.name) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/EpisodeApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/EpisodeApiTest.kt index 8e77119c..f8f2e3e1 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/EpisodeApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/EpisodeApiTest.kt @@ -21,8 +21,8 @@ class EpisodeApiTest : AbstractTest() { assertNull(api.episodes.getEpisode("nonexistant episode", market = market)) assertEquals( - "The Great Inflation (Classic)", - api.episodes.getEpisode("3lMZTE81Pbrp0U12WZe27l", market = market)?.name + "The Midterms Begin With a Texas-Size Showdown", + api.episodes.getEpisode("4fvIbnHhHaD8xljXI0uRXr", market = market)?.name ) } diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicArtistsApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicArtistsApiTest.kt index 91b44488..f63abf0e 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicArtistsApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicArtistsApiTest.kt @@ -51,13 +51,13 @@ class PublicArtistsApiTest : AbstractTest() { assertTrue( api.artists.getArtistAlbums( "7wjeXCtRND2ZdKfMJFu6JC", - 10, include = arrayOf(ArtistApi.AlbumInclusionStrategy.Album) ) .items.asSequence().map { it.name }.contains("Louane") ) } + @Ignore // requires extended quota mode @Test fun testGetRelatedArtists(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetRelatedArtists.name) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicTracksApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicTracksApiTest.kt index 089c09aa..0833799e 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicTracksApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicTracksApiTest.kt @@ -36,6 +36,7 @@ class PublicTracksApiTest : AbstractTest() { ) } + @Ignore // requires extended quota mode @Test fun testAudioAnalysis(): TestResult = runTestOnDefaultDispatcher { buildApi(::testAudioAnalysis.name) @@ -44,6 +45,7 @@ class PublicTracksApiTest : AbstractTest() { assertEquals("165.61333", api.tracks.getAudioAnalysis("0o4jSZBxOQUiDKzMJSqR4x").track.duration.toString()) } + @Ignore // requires extended quota mode @Test fun testAudioFeatures(): TestResult = runTestOnDefaultDispatcher { buildApi(::testAudioFeatures.name) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt index c6795f64..f16ae227 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/UtilityTests.kt @@ -16,9 +16,11 @@ class UtilityTests { fun testPagingObjectGetAllItems(): TestResult = runTestOnDefaultDispatcher { buildSpotifyApi(this::class.simpleName!!, ::testPagingObjectGetAllItems.name)?.let { api = it } - val spotifyWfhPlaylist = api!!.playlists.getPlaylist("37i9dQZF1DWTLSN7iG21yC")!! - val totalTracks = spotifyWfhPlaylist.tracks.total - val allTracks = spotifyWfhPlaylist.tracks.getAllItemsNotNull() + // Getting Spotify-owned playlists is extended-quota-restricted (returns null) +// val spotifyWfhPlaylist = api!!.playlists.getPlaylist("37i9dQZF1DWTLSN7iG21yC")!! + val playlist = api!!.playlists.getPlaylist("4zUEn5Obw0OHZpxXkbeVwg")!! + val totalTracks = playlist.items.total + val allTracks = playlist.items.getAllItemsNotNull() assertEquals(totalTracks, allTracks.size) } @@ -107,7 +109,7 @@ class UtilityTests { api.token = api.token.copy(expiresIn = -1) val currentToken = api.token - api.browse.getAvailableGenreSeeds() + api.albums.getAlbum("16jUwWH1dehPfPlqvHVRtb") assertTrue(test) assertTrue(api.token.accessToken != currentToken.accessToken) From 7165e8f0653768d2346fd59d62060c7f6ac6c9f6 Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 15:27:28 +0100 Subject: [PATCH 04/10] Mark relevant fields as @SpotifyExtendedQuota --- .../endpoints/pub/SearchApi.kt | 8 +++--- .../com.adamratzman.spotify/models/Albums.kt | 21 ++++++++------- .../com.adamratzman.spotify/models/Artists.kt | 5 ++-- .../com.adamratzman.spotify/models/Show.kt | 13 ++++----- .../com.adamratzman.spotify/models/Track.kt | 27 ++++++++++--------- .../com.adamratzman.spotify/models/Users.kt | 13 ++++----- .../spotify/pub/PublicUserApiTest.kt | 2 +- .../spotify/utilities/JsonTests.kt | 18 +++++++++++++ 8 files changed, 66 insertions(+), 41 deletions(-) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/SearchApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/SearchApi.kt index 8596de4c..8fd5599a 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/SearchApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/SearchApi.kt @@ -105,14 +105,14 @@ public open class SearchApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @param filters Optional list of [SearchFilter] to apply to this search. * @param searchTypes A list of item types to search across. Search results include hits from all the specified item types. * @param limit Maximum number of results to return. - Default: 20 + Default: 5 Minimum: 1 - Maximum: 50 + Maximum: 10 Note: The limit is applied within each type, not on the total response. For example, if the limit value is 3 and the type is artist,album, the response contains 3 artists and 3 albums. * @param offset The index of the first result to return. Default: 0 (the first result). - Maximum offset (including limit): 10,00. + Maximum offset (including limit): 1000. Use with limit to get the next page of search results. * @param market If a country code is specified, only artists, albums, and tracks with content that is playable in that market is returned. Note: - Playlist results are not affected by the market parameter. @@ -127,7 +127,7 @@ public open class SearchApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { query: String, vararg searchTypes: SearchType, filters: List = listOf(), - limit: Int? = api.spotifyApiOptions.defaultLimit, + limit: Int? = 5, offset: Int? = null, market: Market? = null, includeExternal: Boolean? = null, diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Albums.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Albums.kt index a4be5102..ae589f01 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Albums.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Albums.kt @@ -2,6 +2,7 @@ package com.adamratzman.spotify.models import com.adamratzman.spotify.SpotifyRestAction +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.utils.Market import com.adamratzman.spotify.utils.match import kotlinx.serialization.SerialName @@ -34,7 +35,7 @@ import kotlinx.serialization.Serializable @Serializable public data class SimpleAlbum( @SerialName("album_type") private val albumTypeString: String, - @SerialName("available_markets") private val availableMarketsString: List = listOf(), + @SpotifyExtendedQuota @SerialName("available_markets") private val availableMarketsString: List = listOf(), @SerialName("external_urls") override val externalUrlsString: Map, override val href: String, override val id: String, @@ -48,9 +49,9 @@ public data class SimpleAlbum( @SerialName("release_date") private val releaseDateString: String? = null, @SerialName("release_date_precision") val releaseDatePrecisionString: String? = null, @SerialName("total_tracks") val totalTracks: Int? = null, - @SerialName("album_group") private val albumGroupString: String? = null + @SpotifyExtendedQuota @SerialName("album_group") private val albumGroupString: String? = null ) : CoreObject() { - val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } + @SpotifyExtendedQuota val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } val albumType: AlbumResultType get() = albumTypeString.let { _ -> @@ -59,7 +60,7 @@ public data class SimpleAlbum( val releaseDate: ReleaseDate? get() = releaseDateString?.let { getReleaseDate(releaseDateString) } - val albumGroup: AlbumResultType? + @SpotifyExtendedQuota val albumGroup: AlbumResultType? get() = albumGroupString?.let { _ -> AlbumResultType.entries.find { it.id == albumGroupString } } @@ -132,8 +133,8 @@ public enum class AlbumResultType(public val id: String) { @Serializable public data class Album( @SerialName("album_type") private val albumTypeString: String, - @SerialName("available_markets") private val availableMarketsString: List = listOf(), - @SerialName("external_ids") private val externalIdsString: Map = hashMapOf(), + @SpotifyExtendedQuota @SerialName("available_markets") private val availableMarketsString: List = listOf(), + @SpotifyExtendedQuota @SerialName("external_ids") private val externalIdsString: Map = hashMapOf(), @SerialName("external_urls") override val externalUrlsString: Map = mapOf(), override val href: String, override val id: String, @@ -143,9 +144,9 @@ public data class Album( val copyrights: List, val genres: List, val images: List? = null, - val label: String, + @SpotifyExtendedQuota val label: String? = null, val name: String, - val popularity: Double, + @SpotifyExtendedQuota val popularity: Double? = null, @SerialName("release_date") private val releaseDateString: String, @SerialName("release_date_precision") val releaseDatePrecision: String, val tracks: PagingObject, @@ -153,9 +154,9 @@ public data class Album( @SerialName("total_tracks") val totalTracks: Int, val restrictions: Restrictions? = null ) : CoreObject() { - val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } + @SpotifyExtendedQuota val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } - val externalIds: List get() = externalIdsString.map { ExternalId(it.key, it.value) } + @SpotifyExtendedQuota val externalIds: List get() = externalIdsString.map { ExternalId(it.key, it.value) } val albumType: AlbumResultType get() = AlbumResultType.entries.first { it.id == albumTypeString } diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Artists.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Artists.kt index 627458ae..ac158387 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Artists.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Artists.kt @@ -2,6 +2,7 @@ package com.adamratzman.spotify.models import com.adamratzman.spotify.SpotifyRestAction +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -57,11 +58,11 @@ public data class Artist( override val id: String, override val uri: ArtistUri, - val followers: Followers, + @SpotifyExtendedQuota val followers: Followers? = null, val genres: List, val images: List? = null, val name: String? = null, - val popularity: Double, + @SpotifyExtendedQuota val popularity: Double? = null, val type: String ) : CoreObject() { override fun getMembersThatNeedApiInstantiation(): List = listOf(this) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Show.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Show.kt index 22383a1e..0ac13d5d 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Show.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Show.kt @@ -2,6 +2,7 @@ package com.adamratzman.spotify.models import com.adamratzman.spotify.SpotifyRestAction +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.utils.Locale import com.adamratzman.spotify.utils.Market import kotlinx.serialization.SerialName @@ -25,7 +26,7 @@ import kotlinx.serialization.Serializable */ @Serializable public data class SimpleShow( - @SerialName("available_markets") private val availableMarketsString: List = listOf(), + @SpotifyExtendedQuota @SerialName("available_markets") private val availableMarketsString: List = listOf(), @SerialName("external_urls") override val externalUrlsString: Map, val copyrights: List, val description: String? = null, @@ -37,11 +38,11 @@ public data class SimpleShow( @SerialName("languages") private val languagesString: List, @SerialName("media_type") val mediaType: String, val name: String, - val publisher: String, + @SpotifyExtendedQuota val publisher: String? = null, val type: String, override val uri: SpotifyUri ) : CoreObject() { - val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } + @SpotifyExtendedQuota val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } val languages: List get() = languagesString.map { Locale.valueOf(it.replace("-", "_")) } @@ -81,7 +82,7 @@ public data class SimpleShow( */ @Serializable public data class Show( - @SerialName("available_markets") private val availableMarketsString: List = listOf(), + @SpotifyExtendedQuota @SerialName("available_markets") private val availableMarketsString: List = listOf(), val copyrights: List, val description: String? = null, val explicit: Boolean, @@ -94,11 +95,11 @@ public data class Show( @SerialName("languages") val languagesString: List, @SerialName("media_type") val mediaType: String, val name: String, - val publisher: String, + @SpotifyExtendedQuota val publisher: String? = null, val type: String, override val uri: ShowUri ) : CoreObject() { - val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } + @SpotifyExtendedQuota val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } val languages: List get() = languagesString.map { Locale.valueOf(it.replace("-", "_")) } diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt index e5284c26..8a7e40d6 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt @@ -2,6 +2,7 @@ package com.adamratzman.spotify.models import com.adamratzman.spotify.SpotifyRestAction +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.utils.Market import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -38,8 +39,8 @@ import kotlinx.serialization.Serializable @Serializable public data class SimpleTrack( @SerialName("external_urls") override val externalUrlsString: Map, - @SerialName("available_markets") private val availableMarketsString: List = listOf(), - @SerialName("external_ids") private val externalIdsString: Map = hashMapOf(), + @SpotifyExtendedQuota @SerialName("available_markets") private val availableMarketsString: List = listOf(), + @SpotifyExtendedQuota @SerialName("external_ids") private val externalIdsString: Map = hashMapOf(), override val href: String, override val id: String, override val uri: SpotifyUri, @@ -49,18 +50,18 @@ public data class SimpleTrack( @SerialName("duration_ms") val durationMs: Int, val explicit: Boolean, @SerialName("is_playable") val isPlayable: Boolean = true, - @SerialName("linked_from") override val linkedTrack: LinkedTrack? = null, + @SpotifyExtendedQuota @SerialName("linked_from") override val linkedTrack: LinkedTrack? = null, val name: String, @SerialName("preview_url") val previewUrl: String? = null, @SerialName("track_number") val trackNumber: Int, val type: String, @SerialName("is_local") val isLocal: Boolean? = null, - val popularity: Double? = null, + @SpotifyExtendedQuota val popularity: Double? = null, val restrictions: Restrictions? = null ) : RelinkingAvailableResponse() { - val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } + @SpotifyExtendedQuota val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } - val externalIds: List get() = externalIdsString.map { ExternalId(it.key, it.value) } + @SpotifyExtendedQuota val externalIds: List get() = externalIdsString.map { ExternalId(it.key, it.value) } val length: Int get() = durationMs @@ -81,6 +82,7 @@ public data class SimpleTrack( public fun toFullTrackRestAction(market: Market? = null): SpotifyRestAction = SpotifyRestAction { toFullTrack(market) } + @OptIn(SpotifyExtendedQuota::class) override fun getMembersThatNeedApiInstantiation(): List = artists + linkedTrack + this } @@ -125,8 +127,8 @@ public data class SimpleTrack( @Serializable public data class Track( @SerialName("external_urls") override val externalUrlsString: Map, - @SerialName("external_ids") private val externalIdsString: Map = hashMapOf(), - @SerialName("available_markets") private val availableMarketsString: List = listOf(), + @SpotifyExtendedQuota @SerialName("external_ids") private val externalIdsString: Map = hashMapOf(), + @SpotifyExtendedQuota @SerialName("available_markets") private val availableMarketsString: List = listOf(), override val href: String, override val id: String, override val uri: PlayableUri, @@ -137,9 +139,9 @@ public data class Track( @SerialName("disc_number") val discNumber: Int, @SerialName("duration_ms") val durationMs: Int, val explicit: Boolean, - @SerialName("linked_from") override val linkedTrack: LinkedTrack? = null, + @SpotifyExtendedQuota @SerialName("linked_from") override val linkedTrack: LinkedTrack? = null, val name: String, - val popularity: Double, + @SpotifyExtendedQuota val popularity: Double? = null, @SerialName("preview_url") val previewUrl: String? = null, @SerialName("track_number") val trackNumber: Int, override val type: String, @@ -149,12 +151,13 @@ public data class Track( val episode: Boolean? = null, val track: Boolean? = null ) : RelinkingAvailableResponse(), Playable { - val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } + @SpotifyExtendedQuota val availableMarkets: List get() = availableMarketsString.map { Market.valueOf(it) } - val externalIds: List get() = externalIdsString.map { ExternalId(it.key, it.value) } + @SpotifyExtendedQuota val externalIds: List get() = externalIdsString.map { ExternalId(it.key, it.value) } val length: Int get() = durationMs + @OptIn(SpotifyExtendedQuota::class) override fun getMembersThatNeedApiInstantiation(): List = artists + album + linkedTrack + this } diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Users.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Users.kt index 8225f2ae..2d8dbb1b 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Users.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Users.kt @@ -2,6 +2,7 @@ package com.adamratzman.spotify.models import com.adamratzman.spotify.SpotifyScope +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -36,13 +37,13 @@ public data class SpotifyUserInformation( override val id: String, override val uri: UserUri, - val country: String? = null, + @SpotifyExtendedQuota val country: String? = null, @SerialName("display_name") val displayName: String? = null, - val email: String? = null, - val followers: Followers, + @SpotifyExtendedQuota val email: String? = null, + @SpotifyExtendedQuota val followers: Followers? = null, val images: List? = null, - val product: String? = null, - @SerialName("explicit_content") val explicitContentSettings: ExplicitContentSettings? = null, + @SpotifyExtendedQuota val product: String? = null, + @SpotifyExtendedQuota @SerialName("explicit_content") val explicitContentSettings: ExplicitContentSettings? = null, val type: String ) : CoreObject() { override fun getMembersThatNeedApiInstantiation(): List = listOf(this) @@ -66,7 +67,7 @@ public data class SpotifyPublicUser( override val uri: UserUri, @SerialName("display_name") val displayName: String? = null, - val followers: Followers = Followers(null, -1), + @SpotifyExtendedQuota val followers: Followers? = Followers(null, -1), val images: List = listOf(), val type: String ) : CoreObject() { diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt index 052e29ab..d79937ce 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt @@ -18,7 +18,7 @@ class PublicUserApiTest : AbstractTest() { fun testPublicUser(): TestResult = runTestOnDefaultDispatcher { buildApi(::testPublicUser.name) - assertTrue(catch { api.users.getProfile("adamratzman1")!!.followers.total } != null) + assertTrue(catch { api.users.getProfile("adamratzman1")!!.followers?.total } != null) assertNull(api.users.getProfile("ejwkfjwkerfjkwerjkfjkwerfjkjksdfjkasdf")) } } diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt index 493343c9..c599c84c 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt @@ -62,6 +62,7 @@ class JsonTests { ) } + @OptIn(SpotifyExtendedQuota::class) @Test fun testArtistDeserialization(): TestResult = runTestOnDefaultDispatcher { if (api == null) buildSpotifyApi(this::class.simpleName!!, ::testArtistDeserialization.name)?.let { api = it } @@ -76,6 +77,23 @@ class JsonTests { assertEquals("artist", artist.type) } + @OptIn(SpotifyExtendedQuota::class) + @Test + fun testAlbumDeserialization(): TestResult = runTestOnDefaultDispatcher { + if (api == null) buildSpotifyApi(this::class.simpleName!!, ::testAlbumDeserialization.name)?.let { api = it } + + // album with removed fields: album_group, available_markets, external_ids, label, popularity + val json = + """{"album_type": "album","total_tracks":18,"external_urls":{"spotify":"https://open.spotify.com/album/4aawyAB9vmqN3uQ7FjRGTy"},"href":"https://api.spotify.com/v1/albums/4aawyAB9vmqN3uQ7FjRGTy?locale=en-GB%2Cen%3Bq%3D0.9","id":"4aawyAB9vmqN3uQ7FjRGTy","images":[{"url":"https://i.scdn.co/image/ab67616d0000b2732c5b24ecfa39523a75c993c4","height":640,"width":640},{"url":"https://i.scdn.co/image/ab67616d00001e022c5b24ecfa39523a75c993c4","height":300,"width":300},{"url":"https://i.scdn.co/image/ab67616d000048512c5b24ecfa39523a75c993c4","height":64,"width":64}],"name":"GlobalWarming","release_date":"2012-11-16","release_date_precision":"day","type":"album","uri":"spotify:album:4aawyAB9vmqN3uQ7FjRGTy","artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"}],"tracks":{"href":"https://api.spotify.com/v1/albums/4aawyAB9vmqN3uQ7FjRGTy/tracks?offset=0&limit=50&locale=en-GB,en;q%3D0.9","limit":50,"next":null,"offset":0,"previous":null,"total":18,"items":[{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/7iJrDbKM5fEkGdm5kpjFzS"},"href":"https://api.spotify.com/v1/artists/7iJrDbKM5fEkGdm5kpjFzS","id":"7iJrDbKM5fEkGdm5kpjFzS","name":"Sensato","type":"artist","uri":"spotify:artist:7iJrDbKM5fEkGdm5kpjFzS"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":85400,"explicit":true,"external_urls":{"spotify":"https://open.spotify.com/track/6OmhkSOpvYBokMKQxpIGx2"},"href":"https://api.spotify.com/v1/tracks/6OmhkSOpvYBokMKQxpIGx2","id":"6OmhkSOpvYBokMKQxpIGx2","name":"GlobalWarming(feat.Sensato)","preview_url":null,"track_number":1,"type":"track","uri":"spotify:track:6OmhkSOpvYBokMKQxpIGx2","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/2L8yW8GIoirHEdeW4bWQXq"},"href":"https://api.spotify.com/v1/artists/2L8yW8GIoirHEdeW4bWQXq","id":"2L8yW8GIoirHEdeW4bWQXq","name":"TJR","type":"artist","uri":"spotify:artist:2L8yW8GIoirHEdeW4bWQXq"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":206120,"explicit":false,"external_urls":{"spotify":"https://open.spotify.com/track/2iblMMIgSznA464mNov7A8"},"href":"https://api.spotify.com/v1/tracks/2iblMMIgSznA464mNov7A8","id":"2iblMMIgSznA464mNov7A8","name":"Don'tStoptheParty(feat.TJR)","preview_url":null,"track_number":2,"type":"track","uri":"spotify:track:2iblMMIgSznA464mNov7A8","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/1l7ZsJRRS8wlW3WfJfPfNS"},"href":"https://api.spotify.com/v1/artists/1l7ZsJRRS8wlW3WfJfPfNS","id":"1l7ZsJRRS8wlW3WfJfPfNS","name":"ChristinaAguilera","type":"artist","uri":"spotify:artist:1l7ZsJRRS8wlW3WfJfPfNS"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":229506,"explicit":false,"external_urls":{"spotify":"https://open.spotify.com/track/4yOn1TEcfsKHUJCL2h1r8I"},"href":"https://api.spotify.com/v1/tracks/4yOn1TEcfsKHUJCL2h1r8I","id":"4yOn1TEcfsKHUJCL2h1r8I","name":"FeelThisMoment(feat.ChristinaAguilera)","preview_url":null,"track_number":3,"type":"track","uri":"spotify:track:4yOn1TEcfsKHUJCL2h1r8I","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":207440,"explicit":false,"external_urls":{"spotify":"https://open.spotify.com/track/7fmpKF0rLGPnP7kcQ5ZMm7"},"href":"https://api.spotify.com/v1/tracks/7fmpKF0rLGPnP7kcQ5ZMm7","id":"7fmpKF0rLGPnP7kcQ5ZMm7","name":"BackinTime-featuredin\"MenInBlack3\"","preview_url":null,"track_number":4,"type":"track","uri":"spotify:track:7fmpKF0rLGPnP7kcQ5ZMm7","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/7bXgB6jMjp9ATFy66eO08Z"},"href":"https://api.spotify.com/v1/artists/7bXgB6jMjp9ATFy66eO08Z","id":"7bXgB6jMjp9ATFy66eO08Z","name":"ChrisBrown","type":"artist","uri":"spotify:artist:7bXgB6jMjp9ATFy66eO08Z"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":221133,"explicit":false,"external_urls":{"spotify":"https://open.spotify.com/track/3jStb2imKd6oUoBT1zq5lp"},"href":"https://api.spotify.com/v1/tracks/3jStb2imKd6oUoBT1zq5lp","id":"3jStb2imKd6oUoBT1zq5lp","name":"HopeWeMeetAgain(feat.ChrisBrown)","preview_url":null,"track_number":5,"type":"track","uri":"spotify:track:3jStb2imKd6oUoBT1zq5lp","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/23zg3TcAtWQy7J6upgbUnj"},"href":"https://api.spotify.com/v1/artists/23zg3TcAtWQy7J6upgbUnj","id":"23zg3TcAtWQy7J6upgbUnj","name":"USHER","type":"artist","uri":"spotify:artist:23zg3TcAtWQy7J6upgbUnj"},{"external_urls":{"spotify":"https://open.spotify.com/artist/4D75GcNG95ebPtNvoNVXhz"},"href":"https://api.spotify.com/v1/artists/4D75GcNG95ebPtNvoNVXhz","id":"4D75GcNG95ebPtNvoNVXhz","name":"AFROJACK","type":"artist","uri":"spotify:artist:4D75GcNG95ebPtNvoNVXhz"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":243160,"explicit":true,"external_urls":{"spotify":"https://open.spotify.com/track/6Q4PYJtrq8CBx7YCY5IyRN"},"href":"https://api.spotify.com/v1/tracks/6Q4PYJtrq8CBx7YCY5IyRN","id":"6Q4PYJtrq8CBx7YCY5IyRN","name":"PartyAin'tOver(feat.Usher&Afrojack)","preview_url":null,"track_number":6,"type":"track","uri":"spotify:track:6Q4PYJtrq8CBx7YCY5IyRN","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/2DlGxzQSjYe5N6G9nkYghR"},"href":"https://api.spotify.com/v1/artists/2DlGxzQSjYe5N6G9nkYghR","id":"2DlGxzQSjYe5N6G9nkYghR","name":"JenniferLopez","type":"artist","uri":"spotify:artist:2DlGxzQSjYe5N6G9nkYghR"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":196920,"explicit":false,"external_urls":{"spotify":"https://open.spotify.com/track/0QTVwqcOsYd73AOkYkk0Hg"},"href":"https://api.spotify.com/v1/tracks/0QTVwqcOsYd73AOkYkk0Hg","id":"0QTVwqcOsYd73AOkYkk0Hg","name":"DrinksforYou(LadiesAnthem)(feat.J.Lo)","preview_url":null,"track_number":7,"type":"track","uri":"spotify:track:0QTVwqcOsYd73AOkYkk0Hg","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/2NhdGz9EDv2FeUw6udu2g1"},"href":"https://api.spotify.com/v1/artists/2NhdGz9EDv2FeUw6udu2g1","id":"2NhdGz9EDv2FeUw6udu2g1","name":"TheWanted","type":"artist","uri":"spotify:artist:2NhdGz9EDv2FeUw6udu2g1"},{"external_urls":{"spotify":"https://open.spotify.com/artist/4D75GcNG95ebPtNvoNVXhz"},"href":"https://api.spotify.com/v1/artists/4D75GcNG95ebPtNvoNVXhz","id":"4D75GcNG95ebPtNvoNVXhz","name":"AFROJACK","type":"artist","uri":"spotify:artist:4D75GcNG95ebPtNvoNVXhz"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":244920,"explicit":true,"external_urls":{"spotify":"https://open.spotify.com/track/10Sydb6AAFPdgCzCKOSZuI"},"href":"https://api.spotify.com/v1/tracks/10Sydb6AAFPdgCzCKOSZuI","id":"10Sydb6AAFPdgCzCKOSZuI","name":"HaveSomeFun(feat.TheWanted&Afrojack)","preview_url":null,"track_number":8,"type":"track","uri":"spotify:track:10Sydb6AAFPdgCzCKOSZuI","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/0e9P96siQmxphVXAwTy2pa"},"href":"https://api.spotify.com/v1/artists/0e9P96siQmxphVXAwTy2pa","id":"0e9P96siQmxphVXAwTy2pa","name":"DannyMercer","type":"artist","uri":"spotify:artist:0e9P96siQmxphVXAwTy2pa"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":206800,"explicit":true,"external_urls":{"spotify":"https://open.spotify.com/track/4k61iDqmtX9nI7RfLmp9aq"},"href":"https://api.spotify.com/v1/tracks/4k61iDqmtX9nI7RfLmp9aq","id":"4k61iDqmtX9nI7RfLmp9aq","name":"OuttaNowhere(feat.DannyMercer)","preview_url":null,"track_number":9,"type":"track","uri":"spotify:track:4k61iDqmtX9nI7RfLmp9aq","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/7qG3b048QCHVRO5Pv1T5lw"},"href":"https://api.spotify.com/v1/artists/7qG3b048QCHVRO5Pv1T5lw","id":"7qG3b048QCHVRO5Pv1T5lw","name":"EnriqueIglesias","type":"artist","uri":"spotify:artist:7qG3b048QCHVRO5Pv1T5lw"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":205800,"explicit":true,"external_urls":{"spotify":"https://open.spotify.com/track/7oGRkL31ElVMcevQDceT99"},"href":"https://api.spotify.com/v1/tracks/7oGRkL31ElVMcevQDceT99","id":"7oGRkL31ElVMcevQDceT99","name":"TchuTchuTcha(feat.EnriqueIglesias)","preview_url":null,"track_number":10,"type":"track","uri":"spotify:track:7oGRkL31ElVMcevQDceT99","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/4D75GcNG95ebPtNvoNVXhz"},"href":"https://api.spotify.com/v1/artists/4D75GcNG95ebPtNvoNVXhz","id":"4D75GcNG95ebPtNvoNVXhz","name":"AFROJACK","type":"artist","uri":"spotify:artist:4D75GcNG95ebPtNvoNVXhz"},{"external_urls":{"spotify":"https://open.spotify.com/artist/1EVWYRr2obCRDoSoD6KSuM"},"href":"https://api.spotify.com/v1/artists/1EVWYRr2obCRDoSoD6KSuM","id":"1EVWYRr2obCRDoSoD6KSuM","name":"HavanaBrown","type":"artist","uri":"spotify:artist:1EVWYRr2obCRDoSoD6KSuM"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":219600,"explicit":true,"external_urls":{"spotify":"https://open.spotify.com/track/60xPqMqnHZl7Jfiu6E9q8X"},"href":"https://api.spotify.com/v1/tracks/60xPqMqnHZl7Jfiu6E9q8X","id":"60xPqMqnHZl7Jfiu6E9q8X","name":"LastNight(feat.Afrojack&HavanaBrown)","preview_url":null,"track_number":11,"type":"track","uri":"spotify:track:60xPqMqnHZl7Jfiu6E9q8X","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":197520,"explicit":true,"external_urls":{"spotify":"https://open.spotify.com/track/1jAdXqOSICyXYLaW9ioSur"},"href":"https://api.spotify.com/v1/tracks/1jAdXqOSICyXYLaW9ioSur","id":"1jAdXqOSICyXYLaW9ioSur","name":"I'mOffThat","preview_url":null,"track_number":12,"type":"track","uri":"spotify:track:1jAdXqOSICyXYLaW9ioSur","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/5F2Bwl7Is7KVwTbNbMclIS"},"href":"https://api.spotify.com/v1/artists/5F2Bwl7Is7KVwTbNbMclIS","id":"5F2Bwl7Is7KVwTbNbMclIS","name":"Papayo","type":"artist","uri":"spotify:artist:5F2Bwl7Is7KVwTbNbMclIS"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":196440,"explicit":false,"external_urls":{"spotify":"https://open.spotify.com/track/0fjRYHFz9ealui1lfnN8it"},"href":"https://api.spotify.com/v1/tracks/0fjRYHFz9ealui1lfnN8it","id":"0fjRYHFz9ealui1lfnN8it","name":"EchaPa'lla(ManosPa'rriba)(feat.Papayo)","preview_url":null,"track_number":13,"type":"track","uri":"spotify:track:0fjRYHFz9ealui1lfnN8it","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/0z4gvV4rjIZ9wHck67ucSV"},"href":"https://api.spotify.com/v1/artists/0z4gvV4rjIZ9wHck67ucSV","id":"0z4gvV4rjIZ9wHck67ucSV","name":"Akon","type":"artist","uri":"spotify:artist:0z4gvV4rjIZ9wHck67ucSV"},{"external_urls":{"spotify":"https://open.spotify.com/artist/5IqWDVLGThjmkm22e3oBU3"},"href":"https://api.spotify.com/v1/artists/5IqWDVLGThjmkm22e3oBU3","id":"5IqWDVLGThjmkm22e3oBU3","name":"DavidRush","type":"artist","uri":"spotify:artist:5IqWDVLGThjmkm22e3oBU3"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":257613,"explicit":true,"external_urls":{"spotify":"https://open.spotify.com/track/7of35ktwTbL906Z1i3mT4K"},"href":"https://api.spotify.com/v1/tracks/7of35ktwTbL906Z1i3mT4K","id":"7of35ktwTbL906Z1i3mT4K","name":"EverybodyFucks(feat.Akon&DavidRush)","preview_url":null,"track_number":14,"type":"track","uri":"spotify:track:7of35ktwTbL906Z1i3mT4K","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/0EmeFodog0BfCgMzAIvKQp"},"href":"https://api.spotify.com/v1/artists/0EmeFodog0BfCgMzAIvKQp","id":"0EmeFodog0BfCgMzAIvKQp","name":"Shakira","type":"artist","uri":"spotify:artist:0EmeFodog0BfCgMzAIvKQp"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":245920,"explicit":false,"external_urls":{"spotify":"https://open.spotify.com/track/2JA6A6Y5f4m7PawM58U2Op"},"href":"https://api.spotify.com/v1/tracks/2JA6A6Y5f4m7PawM58U2Op","id":"2JA6A6Y5f4m7PawM58U2Op","name":"GetItStarted(feat.Shakira)","preview_url":null,"track_number":15,"type":"track","uri":"spotify:track:2JA6A6Y5f4m7PawM58U2Op","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/3BnF35ARlp8mMeyXTjUZsr"},"href":"https://api.spotify.com/v1/artists/3BnF35ARlp8mMeyXTjUZsr","id":"3BnF35ARlp8mMeyXTjUZsr","name":"Vein","type":"artist","uri":"spotify:artist:3BnF35ARlp8mMeyXTjUZsr"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":217680,"explicit":true,"external_urls":{"spotify":"https://open.spotify.com/track/726qZxwhP0jVyIA0ujnnhb"},"href":"https://api.spotify.com/v1/tracks/726qZxwhP0jVyIA0ujnnhb","id":"726qZxwhP0jVyIA0ujnnhb","name":"11:59(feat.Vein)","preview_url":null,"track_number":16,"type":"track","uri":"spotify:track:726qZxwhP0jVyIA0ujnnhb","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/4wLXwxDeWQ8mtUIRPxGiD6"},"href":"https://api.spotify.com/v1/artists/4wLXwxDeWQ8mtUIRPxGiD6","id":"4wLXwxDeWQ8mtUIRPxGiD6","name":"MarcAnthony","type":"artist","uri":"spotify:artist:4wLXwxDeWQ8mtUIRPxGiD6"},{"external_urls":{"spotify":"https://open.spotify.com/artist/4MHssKddnziCghmwBHRiEY"},"href":"https://api.spotify.com/v1/artists/4MHssKddnziCghmwBHRiEY","id":"4MHssKddnziCghmwBHRiEY","name":"Alle","type":"artist","uri":"spotify:artist:4MHssKddnziCghmwBHRiEY"},{"external_urls":{"spotify":"https://open.spotify.com/artist/4Ws2otunReOa6BbwxxpCt6"},"href":"https://api.spotify.com/v1/artists/4Ws2otunReOa6BbwxxpCt6","id":"4Ws2otunReOa6BbwxxpCt6","name":"BennyBenassi","type":"artist","uri":"spotify:artist:4Ws2otunReOa6BbwxxpCt6"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":316480,"explicit":false,"external_urls":{"spotify":"https://open.spotify.com/track/6GPER1Sx8MrBiwWxdulg5Q"},"href":"https://api.spotify.com/v1/tracks/6GPER1Sx8MrBiwWxdulg5Q","id":"6GPER1Sx8MrBiwWxdulg5Q","name":"RainOverMe(feat.MarcAnthony)-BennyBenassiRemix","preview_url":null,"track_number":17,"type":"track","uri":"spotify:track:6GPER1Sx8MrBiwWxdulg5Q","is_local":false},{"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0TnOYISbd1XYRBk9myaseg"},"href":"https://api.spotify.com/v1/artists/0TnOYISbd1XYRBk9myaseg","id":"0TnOYISbd1XYRBk9myaseg","name":"Pitbull","type":"artist","uri":"spotify:artist:0TnOYISbd1XYRBk9myaseg"},{"external_urls":{"spotify":"https://open.spotify.com/artist/7bXgB6jMjp9ATFy66eO08Z"},"href":"https://api.spotify.com/v1/artists/7bXgB6jMjp9ATFy66eO08Z","id":"7bXgB6jMjp9ATFy66eO08Z","name":"ChrisBrown","type":"artist","uri":"spotify:artist:7bXgB6jMjp9ATFy66eO08Z"},{"external_urls":{"spotify":"https://open.spotify.com/artist/5I7l0lSOyusetwCv1aQPMf"},"href":"https://api.spotify.com/v1/artists/5I7l0lSOyusetwCv1aQPMf","id":"5I7l0lSOyusetwCv1aQPMf","name":"JumpSmokers","type":"artist","uri":"spotify:artist:5I7l0lSOyusetwCv1aQPMf"}],"available_markets":["AT","BE","BG","CY","CZ","DE","EE","FI","FR","GR","HU","IE","IT","LV","LT","LU","MT","MX","NL","NO","PL","PT","SK","ES","SE","CH","TR","GB","AD","LI","MC","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","GH","KE","NG","TZ","UG","AM","BW","BF","CV","CW","GM","GE","GW","LS","LR","MW","ML","NA","NE","SM","ST","SN","SC","SL","AZ","BI","CM","TD","KM","GQ","SZ","GA","GN","KG","MR","MN","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","ET","XK"],"disc_number":1,"duration_ms":309626,"explicit":false,"external_urls":{"spotify":"https://open.spotify.com/track/4TWgcICXXfGty8MHGWJ4Ne"},"href":"https://api.spotify.com/v1/tracks/4TWgcICXXfGty8MHGWJ4Ne","id":"4TWgcICXXfGty8MHGWJ4Ne","name":"InternationalLove(feat.ChrisBrown)-JumpSmokersExtendedMix","preview_url":null,"track_number":18,"type":"track","uri":"spotify:track:4TWgcICXXfGty8MHGWJ4Ne","is_local":false}]},"copyrights":[{"text":"(P)2012RCARecords,adivisionofSonyMusicEntertainment","type":"P"}],"genres":[]}""" + val album = Json.decodeFromString(json) + assertEquals(emptyList(), album.availableMarkets) + assertEquals(emptyList(), album.externalIds) + assertNull(album.label) + assertNull(album.popularity) + assertEquals(18, album.totalTracks) + assertTrue(album.tracks.isNotEmpty()) + } + @Test fun testPagingObjectDeserialization() = runTestOnDefaultDispatcher { val json = From 5082e119dbcfa9f9268bdc476506590c4da8098a Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 15:27:34 +0100 Subject: [PATCH 05/10] Migrate playlist endpoints and fields --- .../endpoints/client/ClientFollowingApi.kt | 58 ++++++++- .../endpoints/client/ClientPlaylistApi.kt | 63 +++++---- .../endpoints/pub/PlaylistApi.kt | 2 +- .../models/PagingObjects.kt | 4 +- .../models/Playlist.kt | 20 +-- .../spotify/priv/ClientPlaylistApiTest.kt | 123 +++++++++--------- .../spotify/pub/PublicPlaylistsApiTest.kt | 20 +-- .../spotify/utilities/JsonTests.kt | 27 ++-- 8 files changed, 200 insertions(+), 117 deletions(-) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt index a4df6882..e8897270 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt @@ -40,12 +40,40 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { return isFollowingUsers(user)[0] } + /** + * Check to see if the current Spotify user is following one or more specified playlists. + * + * Checking if the user is privately following a playlist is only possible for the current user when + * that user has granted access to the [SpotifyScope.PlaylistReadPrivate] scope. + * + * Requires the [SpotifyScope.UserLibraryRead] scope. + * + * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/check-library-contains)** + * + * @param playlistIds List of the playlist ids or uris to check. Max 40 + * + * @throws BadRequestException if [playlistIds] contains a non-existing id + * @return A list of booleans corresponding to [playlistIds] of whether the current user is following that playlist + */ + public suspend fun isFollowingPlaylists(vararg playlistIds: String): List { + requireScopes(SpotifyScope.UserLibraryRead) + checkBulkRequesting(40, playlistIds.size) + return bulkStatelessRequest(40, playlistIds.toList()) { chunk -> + get( + endpointBuilder("/me/library/contains") + .with("uris", chunk.joinToString(",") { PlaylistUri(it).uri.encodeUrl() }).toString() + ).toList(ListSerializer(Boolean.serializer()), api, json) + }.flatten() + } + /** * Check to see if the current Spotify user is following the specified playlist. * * Checking if the user is privately following a playlist is only possible for the current user when * that user has granted access to the [SpotifyScope.PlaylistReadPrivate] scope. * + * Requires the [SpotifyScope.UserLibraryRead] scope. + * * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/follow/check-user-following-playlist/)** * * @param playlistId playlist id or uri @@ -328,9 +356,33 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { * * @throws BadRequestException if the playlist is not found */ - public suspend fun unfollowPlaylist(playlist: String): String { - requireScopes(SpotifyScope.PlaylistModifyPublic, SpotifyScope.PlaylistModifyPrivate, anyOf = true) + public suspend fun unfollowPlaylist(playlist: String) { + unfollowPlaylists(playlist) + } - return delete(endpointBuilder("/playlists/${PlaylistUri(playlist).id}/followers").toString()) + /** + * Remove the current user as a follower of one or more playlists. + * + * Unfollowing a publicly followed playlist for a user requires authorization of the [SpotifyScope.PlaylistModifyPublic] scope; + * unfollowing a privately followed playlist requires the [SpotifyScope.PlaylistModifyPrivate] scope. + * + * Note that the scopes you provide relate only to whether the current user is following the playlist publicly or + * privately (i.e. showing others what they are following), not whether the playlist itself is public or private. + * + * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/remove-library-items)** + * + * @param playlistIds The ids or uris of the playlists that are to be no longer followed. Maximum **40**. + * + * @throws BadRequestException if any of the playlist ids are invalid + */ + public suspend fun unfollowPlaylists(vararg playlistIds: String) { + requireScopes(SpotifyScope.PlaylistModifyPublic, SpotifyScope.PlaylistModifyPrivate, anyOf = true) + checkBulkRequesting(40, playlistIds.size) + bulkStatelessRequest(40, playlistIds.toList()) { chunk -> + delete( + endpointBuilder("/me/library") + .with("uris", chunk.joinToString(",") { PlaylistUri(it).uri.encodeUrl() }).toString() + ) + } } } diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlaylistApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlaylistApi.kt index 54a1eeb7..d367e6a2 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlaylistApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlaylistApi.kt @@ -46,7 +46,6 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { * * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/playlists/create-playlist/)** * - * @param user The user’s Spotify user ID. * @param name The name for the new playlist, for example "Your Coolest Playlist" . This name does not need to be * unique; a user may have several playlists with the same name. * @param description @@ -63,7 +62,6 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { description: String? = null, public: Boolean? = null, collaborative: Boolean? = null, - user: String? = null ): Playlist { if (public == null || public) { requireScopes(SpotifyScope.PlaylistModifyPublic) @@ -79,11 +77,25 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { if (collaborative != null) body += buildJsonObject { put("collaborative", collaborative) } return post( - endpointBuilder("/users/${UserUri(user ?: (api as SpotifyClientApi).getUserId()).id.encodeUrl()}/playlists").toString(), + endpointBuilder("/me/playlists").toString(), body.mapToJsonString() ).toObject(Playlist.serializer(), api, json) } + /** + * Deprecated, user parameter is not required anymore. + * + * @see createClientPlaylist + */ + @Deprecated("Moved", ReplaceWith("createClientPlaylist(name, description, public, collaborative)")) + public suspend fun createClientPlaylist( + name: String, + description: String? = null, + public: Boolean? = null, + collaborative: Boolean? = null, + user: String? = null + ): Playlist = createClientPlaylist(name, description, public, collaborative) + /** * Add a [Playable] to a user’s playlist. * @@ -141,7 +153,7 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { } if (position != null) body += buildJsonObject { put("position", position) } post( - endpointBuilder("/playlists/${PlaylistUri(playlist).id.encodeUrl()}/tracks").toString(), + endpointBuilder("/playlists/${PlaylistUri(playlist).id.encodeUrl()}/items").toString(), body.mapToJsonString() ) } @@ -234,9 +246,20 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { * * @param playlist playlist id */ - public suspend fun deleteClientPlaylist(playlist: String): String = + public suspend fun deleteClientPlaylist(playlist: String): Unit = (api as SpotifyClientApi).following.unfollowPlaylist(PlaylistUri(playlist).id) + /** + * This method is equivalent to unfollowing the playlists with the given [playlists]. + * + * Unfortunately, Spotify does not allow **deletion** of playlists themselves + * + * @param playlists playlist ids + */ + public suspend fun deleteClientPlaylists(vararg playlists: String) { + (api as SpotifyClientApi).following.unfollowPlaylists(*playlists) + } + /** * Reorder a playable or a group of playables in a playlist. * @@ -275,7 +298,7 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { if (snapshotId != null) body += buildJsonObject { put("snapshot_id", snapshotId) } return put( - endpointBuilder("/playlists/${PlaylistUri(playlist).id.encodeUrl()}/tracks").toString(), + endpointBuilder("/playlists/${PlaylistUri(playlist).id.encodeUrl()}/items").toString(), body.mapToJsonString() ).toObject(PlaylistSnapshot.serializer(), api, json) } @@ -305,7 +328,7 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { ) } put( - endpointBuilder("/playlists/${PlaylistUri(playlist).id.encodeUrl()}/tracks").toString(), + endpointBuilder("/playlists/${PlaylistUri(playlist).id.encodeUrl()}/items").toString(), body.mapToJsonString() ) } @@ -399,6 +422,7 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { * @param positions The positions at which the playable is located in the playlist * @param snapshotId The playlist snapshot against which to apply this action. **recommended to have** */ + @Deprecated("Removed positions", ReplaceWith("removePlayableFromClientPlaylist(playlist, playable, snapshotId)")) public suspend fun removePlayableFromClientPlaylist( playlist: String, playable: PlayableUri, @@ -440,7 +464,7 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { playlist: String, vararg playables: PlayableUri, snapshotId: String? = null - ): PlaylistSnapshot = removePlaylistPlayablesImpl(playlist, playables.map { it to null }.toTypedArray(), snapshotId) + ): PlaylistSnapshot = removePlaylistPlayablesImpl(playlist, playables.toList().toTypedArray(), snapshotId) /** * Remove playables (each with their own positions) from the given playlist. **Bulk requesting is only available when [snapshotId] is null.** @@ -454,15 +478,16 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { * @param playables An array of [Pair]s of playable uris *and* playable positions (zero-based). Maximum **100**. * @param snapshotId The playlist snapshot against which to apply this action. **recommended to have** */ + @Deprecated("Removed positions", ReplaceWith("removePlayablesFromClientPlaylist(playlist, *playables.map { it.first }.toTypedArray(), snapshotId = snapshotId)")) public suspend fun removePlayablesFromClientPlaylist( playlist: String, vararg playables: Pair, snapshotId: String? = null - ): PlaylistSnapshot = removePlaylistPlayablesImpl(playlist, playables.toList().toTypedArray(), snapshotId) + ): PlaylistSnapshot = removePlaylistPlayablesImpl(playlist, playables.map { it.first }.toList().toTypedArray(), snapshotId) private suspend fun removePlaylistPlayablesImpl( playlist: String, - playables: Array>, + playables: Array, snapshotId: String? ): PlaylistSnapshot { requireScopes(SpotifyScope.PlaylistModifyPublic, SpotifyScope.PlaylistModifyPrivate, anyOf = true) @@ -474,28 +499,18 @@ public class ClientPlaylistApi(api: GenericSpotifyApi) : PlaylistApi(api) { if (snapshotId != null) body += buildJsonObject { put("snapshot_id", snapshotId) } body += buildJsonObject { put( - "tracks", + "items", JsonArray( - chunk.map { (playable, positions) -> + chunk.map { playable -> val json = jsonMap() json += buildJsonObject { put("uri", playable.uri) } - if (positions?.positions?.isNotEmpty() == true) { - json += buildJsonObject { - put( - "positions", - JsonArray( - positions.positions.map(::JsonPrimitive) - ) - ) - } - } JsonObject(json) } ) ) } delete( - endpointBuilder("/playlists/${PlaylistUri(playlist).id}/tracks").toString(), + endpointBuilder("/playlists/${PlaylistUri(playlist).id}/items").toString(), body = body.mapToJsonString() ).toObject(PlaylistSnapshot.serializer(), api, json) }.last() @@ -512,7 +527,9 @@ public data class PlaylistSnapshot(@SerialName("snapshot_id") val snapshotId: St /** * Represents the positions inside a playlist's playables list of where to locate the playable + * Deprecated, removing certain positions is [not supported anymore](https://community.spotify.com/t5/Spotify-for-Developers/Can-t-remove-specific-tracks-from-playlist/td-p/5753116). * * @param positions Playable positions (zero-based) */ +@Deprecated("Removed") public class SpotifyPlayablePositions(public vararg val positions: Int) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/PlaylistApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/PlaylistApi.kt index f78ab32d..dce9c97b 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/PlaylistApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/PlaylistApi.kt @@ -104,7 +104,7 @@ public open class PlaylistApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { offset: Int? = null, market: Market? = null ): PagingObject = get( - endpointBuilder("/playlists/${PlaylistUri(playlist).id.encodeUrl()}/tracks").with("limit", limit) + endpointBuilder("/playlists/${PlaylistUri(playlist).id.encodeUrl()}/items").with("limit", limit) .with("offset", offset).with("market", market?.getSpotifyId()).toString() ) .toNonNullablePagingObject(PlaylistTrack.serializer(), null, api, json) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/PagingObjects.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/PagingObjects.kt index 4f32b131..70c02fd4 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/PagingObjects.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/PagingObjects.kt @@ -580,8 +580,8 @@ internal fun Any.instantiateLateinitsIfPagingObjects(api: GenericSpotifyApi) = w listOf(this.tracks) } is Playlist -> { - this.tracks.itemClass = PlaylistTrack::class - listOf(this.tracks) + this.items.itemClass = PlaylistTrack::class + listOf(this.items) } is SpotifySearchResult -> { this.albums?.itemClass = SimpleAlbum::class diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Playlist.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Playlist.kt index 6f465cef..a4bc239d 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Playlist.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Playlist.kt @@ -23,7 +23,7 @@ import kotlinx.serialization.Serializable * @param primaryColor Unknown. * @param public The playlist’s public/private status: true the playlist is public, false the * playlist is private, null the playlist status is not relevant. - * @param tracks A collection containing a link ( href ) to the Web API endpoint where full details of the + * @param items A collection containing a link ( href ) to the Web API endpoint where full details of the * playlist’s tracks can be retrieved, along with the total number of tracks in the playlist. * @param type The object type: “playlist” * @param description The playlist description. Only returned for modified, verified playlists, otherwise null. @@ -46,10 +46,11 @@ public data class SimplePlaylist( @SerialName("primary_color") val primaryColor: String? = null, val public: Boolean? = null, @SerialName("snapshot_id") private val snapshotIdString: String, - val tracks: PlaylistTrackInfo, + val items: PlaylistTrackInfo, val type: String ) : CoreObject() { val snapshot: PlaylistSnapshot get() = PlaylistSnapshot(snapshotIdString) + @Deprecated("Renamed", ReplaceWith("items")) val tracks: PlaylistTrackInfo get() = items /** * Converts this [SimplePlaylist] into a full [Playlist] object with the given @@ -79,7 +80,7 @@ public data class SimplePlaylist( * @param addedAt The date and time the track was added. Note that some very old playlists may return null in this field. * @param addedBy The Spotify user who added the track. Note that some very old playlists may return null in this field. * @param isLocal Whether this track is a local file or not. - * @param track Information about the track. In rare occasions, this field may be null if this track's API entry is broken. + * @param item Information about the track. In rare occasions, this field may be null if this track's API entry is broken. * **Warning:** if this is a podcast, the track will be null if you are using [SpotifyAppApi]. */ @Serializable @@ -88,9 +89,11 @@ public data class PlaylistTrack( @SerialName("added_at") val addedAt: String? = null, @SerialName("added_by") val addedBy: SpotifyPublicUser? = null, @SerialName("is_local") val isLocal: Boolean? = null, - @Serializable(with = PlayableSerializer::class) val track: Playable? = null, + @Serializable(with = PlayableSerializer::class) val item: Playable? = null, @SerialName("video_thumbnail") val videoThumbnail: VideoThumbnail? = null -) +) { + @Deprecated("Renamed", ReplaceWith("item")) val track: Playable? get() = item +} /** * Represents a Playlist on Spotify @@ -110,7 +113,7 @@ public data class PlaylistTrack( * @param public The playlist’s public/private status: true the playlist is public, false the playlist is private, * null the playlist status is not relevant * a specific playlist version - * @param tracks Information about the tracks of the playlist. + * @param items Information about the tracks of the playlist. * @param type The object type: “playlist” * * @property snapshot The version identifier for the current playlist. Can be supplied in other requests to target @@ -131,12 +134,13 @@ public data class Playlist( val owner: SpotifyPublicUser, val public: Boolean? = null, @SerialName("snapshot_id") private val snapshotIdString: String, - val tracks: PagingObject, + val items: PagingObject, val type: String ) : CoreObject() { val snapshot: PlaylistSnapshot get() = PlaylistSnapshot(snapshotIdString) + @Deprecated("Renamed", ReplaceWith("items")) val tracks: PagingObject get() = items - override fun getMembersThatNeedApiInstantiation(): List = listOf(tracks, this) + override fun getMembersThatNeedApiInstantiation(): List = listOf(items, this) } /** diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientPlaylistApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientPlaylistApiTest.kt index 12e46dc1..0b40d254 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientPlaylistApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/priv/ClientPlaylistApiTest.kt @@ -5,8 +5,6 @@ package com.adamratzman.spotify.priv import com.adamratzman.spotify.AbstractTest import com.adamratzman.spotify.SpotifyClientApi -import com.adamratzman.spotify.SpotifyException -import com.adamratzman.spotify.endpoints.client.SpotifyPlayablePositions import com.adamratzman.spotify.models.Playlist import com.adamratzman.spotify.models.SimplePlaylist import com.adamratzman.spotify.models.toTrackUri @@ -30,16 +28,16 @@ class ClientPlaylistApiTest : AbstractTest() { private suspend fun tearDown() { if (createdPlaylist != null) { coroutineScope { - api.playlists.getClientPlaylists().getAllItemsNotNull() + val playlistIds = api.playlists.getClientPlaylists().getAllItemsNotNull() .filter { it.name == "this is a test playlist" } - .map { - async { - if (api.following.isFollowingPlaylist(it.id)) { - api.playlists.deleteClientPlaylist(it.id) - } - } + .map { it.id } + val deleteIds = api.following.isFollowingPlaylists(*playlistIds.toTypedArray()) + .mapIndexed { index, bool -> + playlistIds[index] to bool } - .awaitAll() + .filter { it.second } + .map { it.first } + api.playlists.deleteClientPlaylists(*deleteIds.toTypedArray()) } } @@ -73,19 +71,22 @@ class ClientPlaylistApiTest : AbstractTest() { init() - val usTop50Uri = "spotify:playlist:37i9dQZEVXbLRQDuF5jeBp" - val globalTop50Uri = "spotify:playlist:37i9dQZEVXbMDoHDwVN2tF" - val globalViral50Uri = "spotify:playlist:37i9dQZEVXbLiRSasKsNU9" + // Getting Spotify-owned playlists is extended-quota-restricted (returns null) +// val usTop50Uri = "spotify:playlist:37i9dQZEVXbLRQDuF5jeBp" +// val globalTop50Uri = "spotify:playlist:37i9dQZEVXbMDoHDwVN2tF" +// val globalViral50Uri = "spotify:playlist:37i9dQZEVXbLiRSasKsNU9" + val playlistUri = "spotify:playlist:4zUEn5Obw0OHZpxXkbeVwg" val tracks = listOf( - async { api.playlists.getPlaylist(usTop50Uri)!!.tracks.getAllItemsNotNull() }, - async { api.playlists.getPlaylist(globalTop50Uri)!!.tracks.getAllItemsNotNull() }, - async { api.playlists.getPlaylist(globalViral50Uri)!!.tracks.getAllItemsNotNull() } - ).awaitAll().flatten().mapNotNull { it.track?.uri } +// async { api.playlists.getPlaylist(usTop50Uri)!!.items.getAllItemsNotNull() }, +// async { api.playlists.getPlaylist(globalTop50Uri)!!.items.getAllItemsNotNull() }, +// async { api.playlists.getPlaylist(globalViral50Uri)!!.items.getAllItemsNotNull() } + async { api.playlists.getPlaylist(playlistUri)!!.items.getAllItemsNotNull() } + ).awaitAll().flatten().mapNotNull { it.item?.uri } api.spotifyApiOptions.allowBulkRequests = true - suspend fun calculatePlaylistSize(): Int? = api.playlists.getClientPlaylist(createdPlaylist!!.id)!!.tracks.total + suspend fun calculatePlaylistSize(): Int? = api.playlists.getClientPlaylist(createdPlaylist!!.id)!!.toFullPlaylist()!!.items.size val sizeBefore = calculatePlaylistSize() ?: 0 api.playlists.addPlayablesToClientPlaylist(createdPlaylist!!.id, playables = tracks.toTypedArray()) assertEquals(sizeBefore + tracks.size, calculatePlaylistSize()) @@ -131,19 +132,19 @@ class ClientPlaylistApiTest : AbstractTest() { assertTrue(updatedPlaylist.public == false) assertEquals("test playlist", updatedPlaylist.name) //assertEquals("description 2", fullPlaylist.description) <-- spotify is flaky about actually having description set - assertTrue(updatedPlaylist.tracks.total == 2 && updatedPlaylist.images?.isNotEmpty() == true) + assertTrue(updatedPlaylist.items.total == 2 && updatedPlaylist.images?.isNotEmpty() == true) api.playlists.reorderClientPlaylistPlayables(updatedPlaylist.id, 1, insertionPoint = 0) updatedPlaylist = api.playlists.getClientPlaylist(createdPlaylist!!.id)!! - assertTrue(updatedPlaylist.toFullPlaylist()?.tracks?.items?.get(0)?.track?.id == "7FjZU7XFs7P9jHI9Z0yRhK") + assertTrue(updatedPlaylist.toFullPlaylist()?.items?.items?.get(0)?.item?.id == "7FjZU7XFs7P9jHI9Z0yRhK") api.playlists.removeAllClientPlaylistPlayables(updatedPlaylist.id) updatedPlaylist = api.playlists.getClientPlaylist(createdPlaylist!!.id)!! - assertTrue(updatedPlaylist.tracks.total == 0) + assertTrue(updatedPlaylist.items.total == 0) tearDown() } @@ -171,21 +172,22 @@ class ClientPlaylistApiTest : AbstractTest() { assertEquals( listOf(playableUriTwo, playableUriTwo), - api.playlists.getPlaylistTracks(createdPlaylist!!.id).items.map { it.track?.uri } + api.playlists.getPlaylistTracks(createdPlaylist!!.id).items.map { it.item?.uri } ) - api.playlists.addPlayableToClientPlaylist(createdPlaylist!!.id, playableUriOne) - - api.playlists.removePlayableFromClientPlaylist( - createdPlaylist!!.id, - playableUriTwo, - SpotifyPlayablePositions(1) - ) - - assertEquals( - listOf(playableUriTwo, playableUriOne), - api.playlists.getPlaylistTracks(createdPlaylist!!.id).items.map { it.track?.uri } - ) + // PlayablePositions are unsupported now (2024) +// api.playlists.addPlayableToClientPlaylist(createdPlaylist!!.id, playableUriOne) +// +// api.playlists.removePlayableFromClientPlaylist( +// createdPlaylist!!.id, +// playableUriTwo, +// SpotifyPlayablePositions(1) +// ) +// +// assertEquals( +// listOf(playableUriTwo, playableUriOne), +// api.playlists.getPlaylistTracks(createdPlaylist!!.id).items.map { it.item?.uri } +// ) api.playlists.setClientPlaylistPlayables( createdPlaylist!!.id, @@ -199,32 +201,33 @@ class ClientPlaylistApiTest : AbstractTest() { assertTrue(api.playlists.getPlaylistTracks(createdPlaylist!!.id).items.isEmpty()) - api.playlists.setClientPlaylistPlayables( - createdPlaylist!!.id, - playableUriTwo, - playableUriOne, - playableUriTwo, - playableUriTwo, - playableUriOne - ) - - api.playlists.removePlayablesFromClientPlaylist( - createdPlaylist!!.id, - Pair(playableUriOne, SpotifyPlayablePositions(4)), - Pair(playableUriTwo, SpotifyPlayablePositions(0)) - ) - - assertEquals( - listOf(playableUriOne, playableUriTwo, playableUriTwo), - api.playlists.getPlaylistTracks(createdPlaylist!!.id).items.map { it.track?.uri } - ) - - assertFailsWith { - api.playlists.removePlayablesFromClientPlaylist( - createdPlaylist!!.id, - Pair(playableUriOne, SpotifyPlayablePositions(3)) - ) - } + // PlayablePositions are unsupported now (2024) +// api.playlists.setClientPlaylistPlayables( +// createdPlaylist!!.id, +// playableUriTwo, +// playableUriOne, +// playableUriTwo, +// playableUriTwo, +// playableUriOne +// ) +// +// api.playlists.removePlayablesFromClientPlaylist( +// createdPlaylist!!.id, +// Pair(playableUriOne, SpotifyPlayablePositions(4)), +// Pair(playableUriTwo, SpotifyPlayablePositions(0)) +// ) +// +// assertEquals( +// listOf(playableUriOne, playableUriTwo, playableUriTwo), +// api.playlists.getPlaylistTracks(createdPlaylist!!.id).items.map { it.item?.uri } +// ) +// +// assertFailsWith { +// api.playlists.removePlayablesFromClientPlaylist( +// createdPlaylist!!.id, +// Pair(playableUriOne, SpotifyPlayablePositions(3)) +// ) +// } tearDown() } diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt index 3cfbd205..5b324904 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt @@ -37,14 +37,14 @@ class PublicPlaylistsApiTest : AbstractTest() { assertEquals("run2", api.playlists.getPlaylist("78eWnYKwDksmCHAjOUNPEj")?.name) assertNull(api.playlists.getPlaylist("nope")) - assertTrue(api.playlists.getPlaylist("78eWnYKwDksmCHAjOUNPEj")!!.tracks.isNotEmpty()) - val playlistWithLocalAndNonLocalTracks = api.playlists.getPlaylist("627gNjNzj3sOrSiDm5acc2")!!.tracks - assertEquals(LocalTrack::class, playlistWithLocalAndNonLocalTracks[0].track!!::class) - assertEquals(Track::class, playlistWithLocalAndNonLocalTracks[1].track!!::class) + assertTrue(api.playlists.getPlaylist("78eWnYKwDksmCHAjOUNPEj")!!.items.isNotEmpty()) + val playlistWithLocalAndNonLocalTracks = api.playlists.getPlaylist("627gNjNzj3sOrSiDm5acc2")!!.items + assertEquals(LocalTrack::class, playlistWithLocalAndNonLocalTracks[0].item!!::class) + assertEquals(Track::class, playlistWithLocalAndNonLocalTracks[1].item!!::class) if (api is SpotifyClientApi) { - val playlistWithPodcastsTracks = api.playlists.getPlaylist("37i9dQZF1DX8tN3OFXtAqt")!!.tracks - assertEquals(PodcastEpisodeTrack::class, playlistWithPodcastsTracks[0].track!!::class) + val playlistWithPodcastsTracks = api.playlists.getPlaylist("38he99wNRz1QU6mrOAeyw9")!!.items + assertEquals(PodcastEpisodeTrack::class, playlistWithPodcastsTracks[0].item!!::class) } } @@ -54,13 +54,13 @@ class PublicPlaylistsApiTest : AbstractTest() { assertTrue(api.playlists.getPlaylistTracks("78eWnYKwDksmCHAjOUNPEj").items.isNotEmpty()) val playlist = api.playlists.getPlaylistTracks("627gNjNzj3sOrSiDm5acc2") - assertEquals(LocalTrack::class, playlist[0].track!!::class) - assertEquals(Track::class, playlist[1].track!!::class) + assertEquals(LocalTrack::class, playlist[0].item!!::class) + assertEquals(Track::class, playlist[1].item!!::class) assertFailsWith { api.playlists.getPlaylistTracks("adskjfjkasdf") } if (api is SpotifyClientApi) { - val playlistWithPodcasts = api.playlists.getPlaylistTracks("37i9dQZF1DX8tN3OFXtAqt") - assertEquals(PodcastEpisodeTrack::class, playlistWithPodcasts[0].track!!::class) + val playlistWithPodcasts = api.playlists.getPlaylistTracks("38he99wNRz1QU6mrOAeyw9") + assertEquals(PodcastEpisodeTrack::class, playlistWithPodcasts[0].item!!::class) } } diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt index c599c84c..0328638e 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt @@ -4,22 +4,15 @@ package com.adamratzman.spotify.utilities import com.adamratzman.spotify.GenericSpotifyApi +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.buildSpotifyApi -import com.adamratzman.spotify.models.Album -import com.adamratzman.spotify.models.Artist -import com.adamratzman.spotify.models.ArtistUri -import com.adamratzman.spotify.models.CursorBasedPagingObject -import com.adamratzman.spotify.models.PagingObject -import com.adamratzman.spotify.models.Track +import com.adamratzman.spotify.models.* import com.adamratzman.spotify.runTestOnDefaultDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult import kotlinx.serialization.builtins.nullable -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue +import kotlin.test.* class JsonTests { var api: GenericSpotifyApi? = null @@ -108,6 +101,20 @@ class JsonTests { assertEquals(5, pagingObject.total) } + @Test + fun testPlaylistDeserialization(): TestResult = runTestOnDefaultDispatcher { + if (api == null) buildSpotifyApi(this::class.simpleName!!, ::testPlaylistDeserialization.name)?.let { api = it } + + // playlist with renamed fields: tracks->items, tracks.tracks->items.items, tracks.tracks.track->items.items.item + val json = + """{"collaborative":false,"description":"Aplaylistfortestingpourposes","external_urls":{"spotify":"https://open.spotify.com/playlist/3cEYpjA9oz9GiPac4AsH4n"},"followers":{"href":null,"total":1479},"href":"https://api.spotify.com/v1/playlists/3cEYpjA9oz9GiPac4AsH4n?locale=en-GB%2Cen%3Bq%3D0.9","id":"3cEYpjA9oz9GiPac4AsH4n","images":[{"height":null,"url":"https://image-cdn-ak.spotifycdn.com/image/ab67706c0000da848d0ce13d55f634e290f744ba","width":null}],"name":"SpotifyWebAPITestingplaylist","owner":{"display_name":"JMPerez²","external_urls":{"spotify":"https://open.spotify.com/user/jmperezperez"},"href":"https://api.spotify.com/v1/users/jmperezperez","id":"jmperezperez","type":"user","uri":"spotify:user:jmperezperez"},"primary_color":null,"public":true,"snapshot_id":"AAAAEur2+1I6iINI0+04uFfVLiVJraUQ","items":{"href":"https://api.spotify.com/v1/playlists/3cEYpjA9oz9GiPac4AsH4n/items?offset=0&limit=100&locale=en-GB,en;q%3D0.9","items":[{"added_at":"2015-01-15T12:39:22Z","added_by":{"external_urls":{"spotify":"https://open.spotify.com/user/jmperezperez"},"href":"https://api.spotify.com/v1/users/jmperezperez","id":"jmperezperez","type":"user","uri":"spotify:user:jmperezperez"},"is_local":false,"primary_color":null,"item":{"preview_url":null,"available_markets":["AR","AU","AT","BE","BO","BR","BG","CA","CL","CO","CR","CY","CZ","DK","DO","DE","EC","EE","SV","FI","FR","GR","GT","HN","HK","HU","IS","IE","IT","LV","LT","LU","MY","MT","MX","NL","NZ","NI","NO","PA","PY","PE","PH","PL","PT","SG","SK","ES","SE","CH","TW","TR","UY","US","GB","AD","LI","MC","ID","JP","TH","VN","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","IN","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","KR","BD","PK","LK","GH","KE","NG","TZ","UG","AG","AM","BS","BB","BZ","BT","BW","BF","CV","CW","DM","FJ","GM","GE","GD","GW","GY","HT","JM","KI","LS","LR","MW","MV","ML","MH","FM","NA","NR","NE","PW","PG","PR","WS","SM","ST","SN","SC","SL","SB","KN","LC","VC","SR","TL","TO","TT","TV","VU","AZ","BN","BI","KH","CM","TD","KM","GQ","SZ","GA","GN","KG","LA","MO","MR","MN","NP","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","VE","ET","XK"],"explicit":false,"type":"track","episode":false,"track":true,"album":{"available_markets":["AR","AU","AT","BE","BO","BR","BG","CA","CL","CO","CR","CY","CZ","DK","DO","DE","EC","EE","SV","FI","FR","GR","GT","HN","HK","HU","IS","IE","IT","LV","LT","LU","MY","MT","MX","NL","NZ","NI","NO","PA","PY","PE","PH","PL","PT","SG","SK","ES","SE","CH","TW","TR","UY","US","GB","AD","LI","MC","ID","JP","TH","VN","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","IN","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","KR","BD","PK","LK","GH","KE","NG","TZ","UG","AG","AM","BS","BB","BZ","BT","BW","BF","CV","CW","DM","FJ","GM","GE","GD","GW","GY","HT","JM","KI","LS","LR","MW","MV","ML","MH","FM","NA","NR","NE","PW","PG","PR","WS","SM","ST","SN","SC","SL","SB","KN","LC","VC","SR","TL","TO","TT","TV","VU","AZ","BN","BI","KH","CM","TD","KM","GQ","SZ","GA","GN","KG","LA","MO","MR","MN","NP","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","VE","ET","XK"],"type":"album","album_type":"compilation","href":"https://api.spotify.com/v1/albums/2pANdqPvxInB0YvcDiw4ko","id":"2pANdqPvxInB0YvcDiw4ko","images":[{"url":"https://i.scdn.co/image/ab67616d0000b273ce6d0eef0c1ce77e5f95bbbc","width":640,"height":640},{"url":"https://i.scdn.co/image/ab67616d00001e02ce6d0eef0c1ce77e5f95bbbc","width":300,"height":300},{"url":"https://i.scdn.co/image/ab67616d00004851ce6d0eef0c1ce77e5f95bbbc","width":64,"height":64}],"name":"ProgressivePsyTrancePicksVol.8","release_date":"2012-04-02","release_date_precision":"day","uri":"spotify:album:2pANdqPvxInB0YvcDiw4ko","artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/0LyfQWJT6nXafLPZqxe9Of"},"href":"https://api.spotify.com/v1/artists/0LyfQWJT6nXafLPZqxe9Of","id":"0LyfQWJT6nXafLPZqxe9Of","name":"VariousArtists","type":"artist","uri":"spotify:artist:0LyfQWJT6nXafLPZqxe9Of"}],"external_urls":{"spotify":"https://open.spotify.com/album/2pANdqPvxInB0YvcDiw4ko"},"total_tracks":20},"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/6eSdhw46riw2OUHgMwR8B5"},"href":"https://api.spotify.com/v1/artists/6eSdhw46riw2OUHgMwR8B5","id":"6eSdhw46riw2OUHgMwR8B5","name":"Odiseo","type":"artist","uri":"spotify:artist:6eSdhw46riw2OUHgMwR8B5"}],"disc_number":1,"track_number":10,"duration_ms":376000,"external_ids":{"isrc":"DEKC41200989"},"external_urls":{"spotify":"https://open.spotify.com/track/4rzfv0JLZfVhOhbSQ8o5jZ"},"href":"https://api.spotify.com/v1/tracks/4rzfv0JLZfVhOhbSQ8o5jZ","id":"4rzfv0JLZfVhOhbSQ8o5jZ","name":"Api","popularity":4,"uri":"spotify:track:4rzfv0JLZfVhOhbSQ8o5jZ","is_local":false},"video_thumbnail":{"url":null}},{"added_at":"2015-01-15T12:40:03Z","added_by":{"external_urls":{"spotify":"https://open.spotify.com/user/jmperezperez"},"href":"https://api.spotify.com/v1/users/jmperezperez","id":"jmperezperez","type":"user","uri":"spotify:user:jmperezperez"},"is_local":false,"primary_color":null,"item":{"preview_url":null,"available_markets":["AR","AU","AT","BE","BO","BR","BG","CA","CL","CO","CR","CY","CZ","DK","DO","DE","EC","EE","SV","FI","FR","GR","GT","HN","HK","HU","IS","IE","IT","LV","LT","LU","MY","MT","MX","NL","NZ","NI","NO","PA","PY","PE","PH","PL","PT","SG","SK","ES","SE","CH","TW","TR","UY","US","GB","AD","LI","MC","ID","JP","TH","VN","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","IN","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","KR","BD","PK","LK","GH","KE","NG","TZ","UG","AG","AM","BS","BB","BZ","BT","BW","BF","CV","CW","DM","FJ","GM","GE","GD","GW","GY","HT","JM","KI","LS","LR","MW","MV","ML","MH","FM","NA","NR","NE","PW","PG","PR","WS","SM","ST","SN","SC","SL","SB","KN","LC","VC","SR","TL","TO","TT","TV","VU","AZ","BN","BI","KH","CM","TD","KM","GQ","SZ","GA","GN","KG","LA","MO","MR","MN","NP","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","VE","ET","XK"],"explicit":false,"type":"track","episode":false,"track":true,"album":{"available_markets":["AR","AU","AT","BE","BO","BR","BG","CA","CL","CO","CR","CY","CZ","DK","DO","DE","EC","EE","SV","FI","FR","GR","GT","HN","HK","HU","IS","IE","IT","LV","LT","LU","MY","MT","MX","NL","NZ","NI","NO","PA","PY","PE","PH","PL","PT","SG","SK","ES","SE","CH","TW","TR","UY","US","GB","AD","LI","MC","ID","JP","TH","VN","RO","IL","ZA","SA","AE","BH","QA","OM","KW","EG","MA","DZ","TN","LB","JO","PS","IN","BY","KZ","MD","UA","AL","BA","HR","ME","MK","RS","SI","KR","BD","PK","LK","GH","KE","NG","TZ","UG","AG","AM","BS","BB","BZ","BT","BW","BF","CV","CW","DM","FJ","GM","GE","GD","GW","GY","HT","JM","KI","LS","LR","MW","MV","ML","MH","FM","NA","NR","NE","PW","PG","PR","WS","SM","ST","SN","SC","SL","SB","KN","LC","VC","SR","TL","TO","TT","TV","VU","AZ","BN","BI","KH","CM","TD","KM","GQ","SZ","GA","GN","KG","LA","MO","MR","MN","NP","RW","TG","UZ","ZW","BJ","MG","MU","MZ","AO","CI","DJ","ZM","CD","CG","IQ","LY","TJ","VE","ET","XK"],"type":"album","album_type":"album","href":"https://api.spotify.com/v1/albums/4hnqM0JK4CM1phwfq1Ldyz","id":"4hnqM0JK4CM1phwfq1Ldyz","images":[{"url":"https://i.scdn.co/image/ab67616d0000b273ee0d0dce888c6c8a70db6e8b","width":640,"height":640},{"url":"https://i.scdn.co/image/ab67616d00001e02ee0d0dce888c6c8a70db6e8b","width":300,"height":300},{"url":"https://i.scdn.co/image/ab67616d00004851ee0d0dce888c6c8a70db6e8b","width":64,"height":64}],"name":"ThisIsHappening","release_date":"2010-05-17","release_date_precision":"day","uri":"spotify:album:4hnqM0JK4CM1phwfq1Ldyz","artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/066X20Nz7iquqkkCW6Jxy6"},"href":"https://api.spotify.com/v1/artists/066X20Nz7iquqkkCW6Jxy6","id":"066X20Nz7iquqkkCW6Jxy6","name":"LCDSoundsystem","type":"artist","uri":"spotify:artist:066X20Nz7iquqkkCW6Jxy6"}],"external_urls":{"spotify":"https://open.spotify.com/album/4hnqM0JK4CM1phwfq1Ldyz"},"total_tracks":9},"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/066X20Nz7iquqkkCW6Jxy6"},"href":"https://api.spotify.com/v1/artists/066X20Nz7iquqkkCW6Jxy6","id":"066X20Nz7iquqkkCW6Jxy6","name":"LCDSoundsystem","type":"artist","uri":"spotify:artist:066X20Nz7iquqkkCW6Jxy6"}],"disc_number":1,"track_number":4,"duration_ms":401440,"external_ids":{"isrc":"US4GE1000022"},"external_urls":{"spotify":"https://open.spotify.com/track/4Cy0NHJ8Gh0xMdwyM9RkQm"},"href":"https://api.spotify.com/v1/tracks/4Cy0NHJ8Gh0xMdwyM9RkQm","id":"4Cy0NHJ8Gh0xMdwyM9RkQm","name":"AllIWant","popularity":50,"uri":"spotify:track:4Cy0NHJ8Gh0xMdwyM9RkQm","is_local":false},"video_thumbnail":{"url":null}},{"added_at":"2015-01-15T12:40:35Z","added_by":{"external_urls":{"spotify":"https://open.spotify.com/user/jmperezperez"},"href":"https://api.spotify.com/v1/users/jmperezperez","id":"jmperezperez","type":"user","uri":"spotify:user:jmperezperez"},"is_local":false,"primary_color":null,"item":{"preview_url":null,"available_markets":[],"explicit":false,"type":"track","episode":false,"track":true,"album":{"available_markets":[],"type":"album","album_type":"album","href":"https://api.spotify.com/v1/albums/2usKFntxa98WHMcyW6xJBz","id":"2usKFntxa98WHMcyW6xJBz","images":[{"url":"https://i.scdn.co/image/ab67616d0000b2738b7447ac3daa1da18811cf7b","width":640,"height":640},{"url":"https://i.scdn.co/image/ab67616d00001e028b7447ac3daa1da18811cf7b","width":300,"height":300},{"url":"https://i.scdn.co/image/ab67616d000048518b7447ac3daa1da18811cf7b","width":64,"height":64}],"name":"GlennHoriuchiTrio/GelennHoriuchiQuartet:Mercy/JumpStart/Endpoints/CurlOut/Earthworks/MindProbe/NullSet/AnotherSpace(A)","release_date":"2011-04-01","release_date_precision":"day","uri":"spotify:album:2usKFntxa98WHMcyW6xJBz","artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/1cZW3NKv2zT57wB9ZFsCSw"},"href":"https://api.spotify.com/v1/artists/1cZW3NKv2zT57wB9ZFsCSw","id":"1cZW3NKv2zT57wB9ZFsCSw","name":"GlennHoriuchi","type":"artist","uri":"spotify:artist:1cZW3NKv2zT57wB9ZFsCSw"},{"external_urls":{"spotify":"https://open.spotify.com/artist/272ArH9SUAlslQqsSgPJA2"},"href":"https://api.spotify.com/v1/artists/272ArH9SUAlslQqsSgPJA2","id":"272ArH9SUAlslQqsSgPJA2","name":"GlennHoriuchiTrio","type":"artist","uri":"spotify:artist:272ArH9SUAlslQqsSgPJA2"}],"external_urls":{"spotify":"https://open.spotify.com/album/2usKFntxa98WHMcyW6xJBz"},"total_tracks":8},"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/1cZW3NKv2zT57wB9ZFsCSw"},"href":"https://api.spotify.com/v1/artists/1cZW3NKv2zT57wB9ZFsCSw","id":"1cZW3NKv2zT57wB9ZFsCSw","name":"GlennHoriuchi","type":"artist","uri":"spotify:artist:1cZW3NKv2zT57wB9ZFsCSw"},{"external_urls":{"spotify":"https://open.spotify.com/artist/272ArH9SUAlslQqsSgPJA2"},"href":"https://api.spotify.com/v1/artists/272ArH9SUAlslQqsSgPJA2","id":"272ArH9SUAlslQqsSgPJA2","name":"GlennHoriuchiTrio","type":"artist","uri":"spotify:artist:272ArH9SUAlslQqsSgPJA2"}],"disc_number":1,"track_number":2,"duration_ms":358760,"external_ids":{"isrc":"USB8U1025969"},"external_urls":{"spotify":"https://open.spotify.com/track/6hvFrZNocdt2FcKGCSY5NI"},"href":"https://api.spotify.com/v1/tracks/6hvFrZNocdt2FcKGCSY5NI","id":"6hvFrZNocdt2FcKGCSY5NI","name":"Endpoints","popularity":0,"uri":"spotify:track:6hvFrZNocdt2FcKGCSY5NI","is_local":false},"video_thumbnail":{"url":null}},{"added_at":"2015-01-15T12:41:10Z","added_by":{"external_urls":{"spotify":"https://open.spotify.com/user/jmperezperez"},"href":"https://api.spotify.com/v1/users/jmperezperez","id":"jmperezperez","type":"user","uri":"spotify:user:jmperezperez"},"is_local":false,"primary_color":null,"item":{"preview_url":null,"available_markets":[],"explicit":false,"type":"track","episode":false,"track":true,"album":{"available_markets":[],"type":"album","album_type":"album","href":"https://api.spotify.com/v1/albums/0ivM6kSawaug0j3tZVusG2","id":"0ivM6kSawaug0j3tZVusG2","images":[{"url":"https://i.scdn.co/image/ab67616d0000b27304e57d181ff062f8339d6c71","width":640,"height":640},{"url":"https://i.scdn.co/image/ab67616d00001e0204e57d181ff062f8339d6c71","width":300,"height":300},{"url":"https://i.scdn.co/image/ab67616d0000485104e57d181ff062f8339d6c71","width":64,"height":64}],"name":"AllTheBest(SpanishVersion)","release_date":"2007-01-01","release_date_precision":"day","uri":"spotify:album:0ivM6kSawaug0j3tZVusG2","artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/2KftmGt9sk1yLjsAoloC3M"},"href":"https://api.spotify.com/v1/artists/2KftmGt9sk1yLjsAoloC3M","id":"2KftmGt9sk1yLjsAoloC3M","name":"Zucchero","type":"artist","uri":"spotify:artist:2KftmGt9sk1yLjsAoloC3M"}],"external_urls":{"spotify":"https://open.spotify.com/album/0ivM6kSawaug0j3tZVusG2"},"total_tracks":18},"artists":[{"external_urls":{"spotify":"https://open.spotify.com/artist/2KftmGt9sk1yLjsAoloC3M"},"href":"https://api.spotify.com/v1/artists/2KftmGt9sk1yLjsAoloC3M","id":"2KftmGt9sk1yLjsAoloC3M","name":"Zucchero","type":"artist","uri":"spotify:artist:2KftmGt9sk1yLjsAoloC3M"}],"disc_number":1,"track_number":18,"duration_ms":176093,"external_ids":{"isrc":"ITUM70701043"},"external_urls":{"spotify":"https://open.spotify.com/track/2E2znCPaS8anQe21GLxcvJ"},"href":"https://api.spotify.com/v1/tracks/2E2znCPaS8anQe21GLxcvJ","id":"2E2znCPaS8anQe21GLxcvJ","name":"YouAreSoBeautiful","popularity":0,"uri":"spotify:track:2E2znCPaS8anQe21GLxcvJ","is_local":false},"video_thumbnail":{"url":null}}],"limit":100,"next":null,"offset":0,"previous":null,"total":5},"type":"playlist","uri":"spotify:playlist:3cEYpjA9oz9GiPac4AsH4n"}""" + val playlist = Json.decodeFromString(json) + assertEquals(5, playlist.tracks.total) + assertEquals(5, playlist.items.total) + assertEquals("Api", playlist.items.items[0].item?.asTrack?.name) + println(playlist) + } + @Test fun testCursorBasedPagingObjectDeserialization() = runTestOnDefaultDispatcher { val json = From df4b96504a83fa1c29822aa00ca9d96ffee134dd Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 15:27:36 +0100 Subject: [PATCH 06/10] Migrate to /me/library endpoints --- .../endpoints/client/ClientFollowingApi.kt | 91 ++++++++++--------- .../endpoints/client/ClientLibraryApi.kt | 22 +++-- .../endpoints/pub/FollowingApi.kt | 2 + 3 files changed, 60 insertions(+), 55 deletions(-) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt index e8897270..6427a2ac 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt @@ -2,15 +2,10 @@ package com.adamratzman.spotify.endpoints.client import com.adamratzman.spotify.GenericSpotifyApi -import com.adamratzman.spotify.SpotifyClientApi import com.adamratzman.spotify.SpotifyException.BadRequestException import com.adamratzman.spotify.SpotifyScope import com.adamratzman.spotify.endpoints.pub.FollowingApi -import com.adamratzman.spotify.models.Artist -import com.adamratzman.spotify.models.ArtistUri -import com.adamratzman.spotify.models.CursorBasedPagingObject -import com.adamratzman.spotify.models.PlaylistUri -import com.adamratzman.spotify.models.UserUri +import com.adamratzman.spotify.models.* import com.adamratzman.spotify.models.serialization.toCursorBasedPagingObject import com.adamratzman.spotify.models.serialization.toList import com.adamratzman.spotify.utils.encodeUrl @@ -84,10 +79,7 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { * @return Whether the current user is following [playlistId] */ public suspend fun isFollowingPlaylist(playlistId: String): Boolean { - return isFollowingPlaylist( - playlistId, - (api as SpotifyClientApi).getUserId() - ) + return isFollowingPlaylists(playlistId)[0] } /** @@ -97,18 +89,18 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { * * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/follow/check-current-user-follows/)** * - * @param users List of the user Spotify IDs to check. Max 50 + * @param users List of the user Spotify IDs to check. Max 40 * * @throws BadRequestException if [users] contains a non-existing id * @return A list of booleans corresponding to [users] of whether the current user is following that user */ public suspend fun isFollowingUsers(vararg users: String): List { requireScopes(SpotifyScope.UserFollowRead) - checkBulkRequesting(50, users.size) - return bulkStatelessRequest(50, users.toList()) { chunk -> + checkBulkRequesting(40, users.size) + return bulkStatelessRequest(40, users.toList()) { chunk -> get( - endpointBuilder("/me/following/contains").with("type", "user") - .with("ids", chunk.joinToString(",") { UserUri(it).id.encodeUrl() }).toString() + endpointBuilder("/me/library/contains") + .with("uris", chunk.joinToString(",") { UserUri(it).uri.encodeUrl() }).toString() ).toList(ListSerializer(Boolean.serializer()), api, json) }.flatten() } @@ -134,18 +126,18 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { * * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/follow/check-current-user-follows/)** * - * @param artists List of the artist ids or uris to check. Max 50 + * @param artists List of the artist ids or uris to check. Max 40 * * @throws BadRequestException if [artists] contains a non-existing id * @return A list of booleans corresponding to [artists] of whether the current user is following that artist */ public suspend fun isFollowingArtists(vararg artists: String): List { requireScopes(SpotifyScope.UserFollowRead) - checkBulkRequesting(50, artists.size) - return bulkStatelessRequest(50, artists.toList()) { chunk -> + checkBulkRequesting(40, artists.size) + return bulkStatelessRequest(40, artists.toList()) { chunk -> get( - endpointBuilder("/me/following/contains").with("type", "artist") - .with("ids", chunk.joinToString(",") { ArtistUri(it).id.encodeUrl() }).toString() + endpointBuilder("/me/library/contains") + .with("uris", chunk.joinToString(",") { ArtistUri(it).uri.encodeUrl() }).toString() ).toList(ListSerializer(Boolean.serializer()), api, json) }.flatten() } @@ -194,17 +186,17 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { * * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/follow/follow-artists-users/)** * - * @param users User ids or uris. Maximum **50**. + * @param users User ids or uris. Maximum **40**. * * @throws BadRequestException if an invalid id is provided */ public suspend fun followUsers(vararg users: String) { requireScopes(SpotifyScope.UserFollowModify) - checkBulkRequesting(50, users.size) - bulkStatelessRequest(50, users.toList()) { chunk -> + checkBulkRequesting(40, users.size) + bulkStatelessRequest(40, users.toList()) { chunk -> put( - endpointBuilder("/me/following").with("type", "user") - .with("ids", chunk.joinToString(",") { UserUri(it).id.encodeUrl() }).toString() + endpointBuilder("/me/library") + .with("uris", chunk.joinToString(",") { UserUri(it).uri.encodeUrl() }).toString() ) } } @@ -227,17 +219,17 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { * * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/follow/follow-artists-users/)** * - * @param artists User ids or uris. Maximum **50**. + * @param artists User ids or uris. Maximum **40**. * * @throws BadRequestException if an invalid id is provided */ public suspend fun followArtists(vararg artists: String) { requireScopes(SpotifyScope.UserFollowModify) - checkBulkRequesting(50, artists.size) - bulkStatelessRequest(50, artists.toList()) { chunk -> + checkBulkRequesting(40, artists.size) + bulkStatelessRequest(40, artists.toList()) { chunk -> put( - endpointBuilder("/me/following").with("type", "artist") - .with("ids", chunk.joinToString(",") { ArtistUri(it).id.encodeUrl() }).toString() + endpointBuilder("/me/library") + .with("uris", chunk.joinToString(",") { ArtistUri(it).uri.encodeUrl() }).toString() ) } } @@ -256,20 +248,29 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { * * @param playlist the id or uri of the playlist. Any playlist can be followed, regardless of its * public/private status, as long as you know its playlist ID. - * @param followPublicly Defaults to true. If true the playlist will be included in user’s public playlists, - * if false it will remain private. To be able to follow playlists privately, the user must have granted the playlist-modify-private scope. * * @throws BadRequestException if the playlist is not found */ - public suspend fun followPlaylist(playlist: String, followPublicly: Boolean = true): String { + public suspend fun followPlaylist(playlist: String): String { requireScopes(SpotifyScope.PlaylistModifyPublic, SpotifyScope.PlaylistModifyPrivate, anyOf = true) return put( - endpointBuilder("/playlists/${PlaylistUri(playlist).id}/followers").toString(), - "{\"public\": $followPublicly}" + endpointBuilder("/me/library") + .with("uris", listOf(PlaylistUri(playlist).uri.encodeUrl()).joinToString(",")) + .toString() ) } + /** + * Deprecated, followPublicly parameter is not implemented anymore + * + * @see followPlaylist + */ + @Deprecated("Moved", ReplaceWith("followPlaylist(playlist)")) + public suspend fun followPlaylist(playlist: String, followPublicly: Boolean = true): String { + return followPlaylist(playlist) + } + /** * Remove the current user as a follower of another user * @@ -290,17 +291,17 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { * * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/follow/unfollow-artists-users/)** * - * @param users The users to be unfollowed from. Maximum **50**. + * @param users The users to be unfollowed from. Maximum **40**. * * @throws BadRequestException if an invalid id is provided */ public suspend fun unfollowUsers(vararg users: String) { requireScopes(SpotifyScope.UserFollowModify) - checkBulkRequesting(50, users.size) - bulkStatelessRequest(50, users.toList()) { list -> + checkBulkRequesting(40, users.size) + bulkStatelessRequest(40, users.toList()) { list -> delete( - endpointBuilder("/me/following").with("type", "user") - .with("ids", list.joinToString(",") { UserUri(it).id.encodeUrl() }).toString() + endpointBuilder("/me/library") + .with("uris", list.joinToString(",") { UserUri(it).uri.encodeUrl() }).toString() ) } } @@ -325,18 +326,18 @@ public class ClientFollowingApi(api: GenericSpotifyApi) : FollowingApi(api) { * * **[Api Reference](https://developer.spotify.com/documentation/web-api/reference/follow/unfollow-artists-users/)** * - * @param artists The artists to be unfollowed from. Maximum **50**. + * @param artists The artists to be unfollowed from. Maximum **40**. * * * @throws BadRequestException if an invalid id is provided */ public suspend fun unfollowArtists(vararg artists: String) { requireScopes(SpotifyScope.UserFollowModify) - checkBulkRequesting(50, artists.size) - bulkStatelessRequest(50, artists.toList()) { list -> + checkBulkRequesting(40, artists.size) + bulkStatelessRequest(40, artists.toList()) { list -> delete( - endpointBuilder("/me/following").with("type", "artist") - .with("ids", list.joinToString(",") { ArtistUri(it).id.encodeUrl() }).toString() + endpointBuilder("/me/library") + .with("uris", list.joinToString(",") { ArtistUri(it).uri.encodeUrl() }).toString() ) } } diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientLibraryApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientLibraryApi.kt index d583960a..c0f2f779 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientLibraryApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientLibraryApi.kt @@ -171,7 +171,7 @@ public class ClientLibraryApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { } return ids.toList().chunked(50).map { list -> get( - endpointBuilder("/me/$type/contains").with("ids", list.joinToString(",") { type.id(it).encodeUrl() }) + endpointBuilder("/me/library/contains").with("uris", list.joinToString(",") { type.uri(it).encodeUrl() }) .toString() ).toList(ListSerializer(Boolean.serializer()), api, json) }.flatten() @@ -213,7 +213,8 @@ public class ClientLibraryApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { ) } ids.toList().chunked(50).forEach { list -> - put(endpointBuilder("/me/$type").with("ids", list.joinToString(",") { type.id(it).encodeUrl() }).toString()) + put(endpointBuilder("/me/library") + .with("uris", list.joinToString(",") { type.uri(it).encodeUrl() }).toString()) } } @@ -258,9 +259,9 @@ public class ClientLibraryApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { } ids.toList().chunked(50).forEach { list -> delete( - endpointBuilder("/me/$type").with( - "ids", - list.joinToString(",") { type.id(it).encodeUrl() } + endpointBuilder("/me/library").with( + "uris", + list.joinToString(",") { type.uri(it).encodeUrl() } ).toString() ) } @@ -273,11 +274,12 @@ public class ClientLibraryApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * @param value Spotify id for the type * @param id How to transform an id (or uri) input into its Spotify id */ -public enum class LibraryType(private val value: String, internal val id: (String) -> String) { - Track("tracks", { PlayableUri(it).id }), - Album("albums", { AlbumUri(it).id }), - Episode("episodes", { EpisodeUri(it).id }), - Show("shows", { ShowUri(it).id }); +public enum class LibraryType(private val value: String, internal val id: (String) -> String, + internal val uri: (String) -> String) { + Track("tracks", { PlayableUri(it).id }, { PlayableUri(it).uri }), + Album("albums", { AlbumUri(it).id }, { AlbumUri(it).uri }), + Episode("episodes", { EpisodeUri(it).id }, { EpisodeUri(it).uri }), + Show("shows", { ShowUri(it).id }, { ShowUri(it).uri }); override fun toString(): String = value } diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/FollowingApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/FollowingApi.kt index 31469926..6e0f7622 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/FollowingApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/endpoints/pub/FollowingApi.kt @@ -29,6 +29,7 @@ public open class FollowingApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @throws [BadRequestException] if the playlist is not found OR any user in the list does not exist */ + @Deprecated("Removed") public suspend fun areFollowingPlaylist( playlist: String, vararg users: String @@ -55,6 +56,7 @@ public open class FollowingApi(api: GenericSpotifyApi) : SpotifyEndpoint(api) { * * @throws [BadRequestException] if the playlist is not found or if the user does not exist */ + @Deprecated("Moved", ReplaceWith("ClientFollowingApi.isFollowingPlaylist()")) public suspend fun isFollowingPlaylist(playlist: String, user: String): Boolean = areFollowingPlaylist( playlist, users = arrayOf(user) From f6aaf9aeaa6acd54326d697b705de67c8b56e62d Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 15:27:39 +0100 Subject: [PATCH 07/10] Ignore tests requiring extended quota --- .../com.adamratzman/spotify/pub/BrowseApiTest.kt | 12 +++++------- .../com.adamratzman/spotify/pub/EpisodeApiTest.kt | 1 + .../com.adamratzman/spotify/pub/MarketsApiTest.kt | 2 ++ .../spotify/pub/PublicAlbumsApiTest.kt | 11 +++++++++-- .../spotify/pub/PublicArtistsApiTest.kt | 13 +++++++++++-- .../spotify/pub/PublicPlaylistsApiTest.kt | 3 +++ .../spotify/pub/PublicTracksApiTest.kt | 2 ++ .../spotify/pub/PublicUserApiTest.kt | 2 ++ .../com.adamratzman/spotify/pub/ShowApiTest.kt | 1 + 9 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/BrowseApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/BrowseApiTest.kt index c33b3f6f..0ca3899f 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/BrowseApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/BrowseApiTest.kt @@ -13,13 +13,7 @@ import com.adamratzman.spotify.utils.Market import com.adamratzman.spotify.utils.getCurrentTimeMs import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNotSame -import kotlin.test.assertTrue +import kotlin.test.* class BrowseApiTest : AbstractTest() { @Ignore // requires extended quota mode @@ -29,6 +23,7 @@ class BrowseApiTest : AbstractTest() { assertTrue(api.browse.getAvailableGenreSeeds().isNotEmpty()) } + @Ignore // requires extended quota mode @Test fun testGetCategoryList(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetCategoryList.name) @@ -41,6 +36,7 @@ class BrowseApiTest : AbstractTest() { assertTrue(api.browse.getCategoryList(4, 3, locale = Locale.FR_FR, market = Market.CA).items.isNotEmpty()) } + @Ignore // requires extended quota mode @Test fun testGetCategory(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetCategory.name) @@ -53,6 +49,7 @@ class BrowseApiTest : AbstractTest() { assertFailsWith { api.browse.getCategory("no u", Market.US) } } + @Ignore // requires extended quota mode @Test fun testGetPlaylistsByCategory(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetPlaylistsByCategory.name) @@ -89,6 +86,7 @@ class BrowseApiTest : AbstractTest() { assertTrue(api.browse.getFeaturedPlaylists(offset = 32).playlists.total > 0) } + @Ignore // requires extended quota mode @Test fun testGetNewReleases(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetNewReleases.name) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/EpisodeApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/EpisodeApiTest.kt index f8f2e3e1..6065a91b 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/EpisodeApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/EpisodeApiTest.kt @@ -26,6 +26,7 @@ class EpisodeApiTest : AbstractTest() { ) } + @Ignore // requires extended quota mode //@Test //todo re-enable. Flaky test disabled due to infrequent spotify 500s fun testGetEpisodes(): TestResult = runTestOnDefaultDispatcher { diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/MarketsApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/MarketsApiTest.kt index 2e5954ec..6c0b42fa 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/MarketsApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/MarketsApiTest.kt @@ -8,10 +8,12 @@ import com.adamratzman.spotify.GenericSpotifyApi import com.adamratzman.spotify.runTestOnDefaultDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertTrue class MarketsApiTest : AbstractTest() { + @Ignore // requires extended quota mode @Test fun testGetAvailableMarkets(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetAvailableMarkets.name) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicAlbumsApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicAlbumsApiTest.kt index 5caba956..f9bfd037 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicAlbumsApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicAlbumsApiTest.kt @@ -10,6 +10,7 @@ import com.adamratzman.spotify.runTestOnDefaultDispatcher import com.adamratzman.spotify.utils.Market import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -20,13 +21,19 @@ import kotlin.test.assertTrue class PublicAlbumsApiTest : AbstractTest() { @Test - fun testGetAlbums(): TestResult = runTestOnDefaultDispatcher { - buildApi(::testGetAlbums.name) + fun testGetAlbum(): TestResult = runTestOnDefaultDispatcher { + buildApi(::testGetAlbum.name) assertNull(api.albums.getAlbum("asdf", Market.FR)) assertNull(api.albums.getAlbum("asdf")) assertNotNull(api.albums.getAlbum("1f1C1CjidKcWQyiIYcMvP2")) assertNotNull(api.albums.getAlbum("1f1C1CjidKcWQyiIYcMvP2", Market.US)) + } + + @Ignore // requires extended quota mode + @Test + fun testGetAlbums(): TestResult = runTestOnDefaultDispatcher { + buildApi(::testGetAlbums.name) assertFailsWith { api.albums.getAlbums(market = Market.US) } assertFailsWith { api.albums.getAlbums() } diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicArtistsApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicArtistsApiTest.kt index f63abf0e..f5ca62b8 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicArtistsApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicArtistsApiTest.kt @@ -11,6 +11,7 @@ import com.adamratzman.spotify.runTestOnDefaultDispatcher import com.adamratzman.spotify.utils.Market import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -20,11 +21,18 @@ import kotlin.test.assertTrue class PublicArtistsApiTest : AbstractTest() { @Test - fun testGetArtists(): TestResult = runTestOnDefaultDispatcher { - buildApi(::testGetArtists.name) + fun testGetArtist(): TestResult = runTestOnDefaultDispatcher { + buildApi(::testGetArtist.name) assertNull(api.artists.getArtist("adkjlasdf")) assertNotNull(api.artists.getArtist("66CXWjxzNUsdJxJ2JdwvnR")) + } + + @Ignore // requires extended quota mode + @Test + fun testGetArtists(): TestResult = runTestOnDefaultDispatcher { + buildApi(::testGetArtists.name) + assertFailsWith { api.artists.getArtists() } assertEquals( listOf(true, true), @@ -67,6 +75,7 @@ class PublicArtistsApiTest : AbstractTest() { assertTrue(api.artists.getRelatedArtists("0X2BH1fck6amBIoJhDVmmJ").isNotEmpty()) } + @Ignore // requires extended quota mode @Test fun testGetArtistTopTracksByMarket(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetArtistTopTracksByMarket.name) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt index 5b324904..4f1a4f8c 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicPlaylistsApiTest.kt @@ -13,6 +13,7 @@ import com.adamratzman.spotify.models.Track import com.adamratzman.spotify.runTestOnDefaultDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -20,6 +21,7 @@ import kotlin.test.assertNull import kotlin.test.assertTrue class PublicPlaylistsApiTest : AbstractTest() { + @Ignore // requires extended quota mode @Test fun testGetUserPlaylists(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetUserPlaylists.name) @@ -72,6 +74,7 @@ class PublicPlaylistsApiTest : AbstractTest() { assertFailsWith { api.playlists.getPlaylistCovers("adskjfjkasdf") } } + @Ignore // requires extended quota mode @Test fun testConvertSimplePlaylistToPlaylist(): TestResult = runTestOnDefaultDispatcher { buildApi(::testConvertSimplePlaylistToPlaylist.name) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicTracksApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicTracksApiTest.kt index 0833799e..43d3f21f 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicTracksApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicTracksApiTest.kt @@ -10,6 +10,7 @@ import com.adamratzman.spotify.runTestOnDefaultDispatcher import com.adamratzman.spotify.utils.Market import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -25,6 +26,7 @@ class PublicTracksApiTest : AbstractTest() { assertNull(api.tracks.getTrack("nonexistant track")) } + @Ignore // requires extended quota mode @Test fun testGetTracks(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetTracks.name) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt index d79937ce..e380ca9b 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/PublicUserApiTest.kt @@ -9,11 +9,13 @@ import com.adamratzman.spotify.runTestOnDefaultDispatcher import com.adamratzman.spotify.utils.catch import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult +import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertNull import kotlin.test.assertTrue class PublicUserApiTest : AbstractTest() { + @Ignore // requires extended quota mode @Test fun testPublicUser(): TestResult = runTestOnDefaultDispatcher { buildApi(::testPublicUser.name) diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/pub/ShowApiTest.kt b/src/commonTest/kotlin/com.adamratzman/spotify/pub/ShowApiTest.kt index 3cdd2e9b..9e1a302a 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/pub/ShowApiTest.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/pub/ShowApiTest.kt @@ -26,6 +26,7 @@ class ShowApiTest : AbstractTest() { ) } + @Ignore // requires extended quota mode @Test fun testGetShows(): TestResult = runTestOnDefaultDispatcher { buildApi(::testGetShows.name) From 9c42efb42411a43ea87b6fb7644fa12fdd7cb6f2 Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 15:27:41 +0100 Subject: [PATCH 08/10] Mark implicit grant clients as deprecated --- .../kotlin/com.adamratzman.spotify/SpotifyApi.kt | 1 + .../com.adamratzman.spotify/SpotifyApiBuilder.kt | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt index 53d26b21..d28dd757 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt @@ -657,6 +657,7 @@ public open class SpotifyClientApi( * An API instance created through implicit grant flow, with access to private information * managed through the scopes exposed in [token]. [token] is not refreshable and is only accessible for limited time. */ +@Deprecated("Removed") public class SpotifyImplicitGrantApi( clientId: String?, token: Token, diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt index cba12f8b..6257671e 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApiBuilder.kt @@ -27,7 +27,7 @@ import kotlinx.serialization.json.Json * @param scopes Spotify scopes the api instance should be able to access for the user * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/) * @param redirectUri Spotify [redirect uri](https://developer.spotify.com/documentation/general/guides/app-settings/) - * @param isImplicitGrantFlow Whether the authorization url should be for the Implicit Grant flow, otherwise for Authorization Code flo + * @param isImplicitGrantFlow Whether the authorization url should be for the Implicit Grant flow, otherwise for Authorization Code flow (Deprecated) * @param shouldShowDialog If [isImplicitGrantFlow] is true, whether or not to force the user to approve the app again if they’ve already done so. * @param state This provides protection against attacks such as cross-site request forgery. */ @@ -107,11 +107,14 @@ public fun getSpotifyPkceCodeChallenge(codeVerifier: String): String { * * Use case: I have a token obtained after implicit grant authorization. * + * * **Deprecated**. Read how to migrate [here](https://developer.spotify.com/documentation/web-api/tutorials/migration-implicit-auth-code). + * * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/) * @param token Token created from the hash response in the implicit grant callback * * @return [SpotifyImplicitGrantApi] that can immediately begin making calls */ +@Deprecated("Removed") public fun spotifyImplicitGrantApi( clientId: String?, token: Token @@ -127,12 +130,15 @@ public fun spotifyImplicitGrantApi( * * Use case: I have a token obtained after implicit grant authorization. * + * **Deprecated**. Read how to migrate [here](https://developer.spotify.com/documentation/web-api/tutorials/migration-implicit-auth-code). + * * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/) * @param token Token created from the hash response in the implicit grant callback * @param block Block to set API options * * @return [SpotifyImplicitGrantApi] that can immediately begin making calls */ +@Deprecated("Removed") public fun spotifyImplicitGrantApi( clientId: String?, token: Token, @@ -1134,7 +1140,7 @@ public class SpotifyUserAuthorization( * @param requestTimeoutMillis The maximum time, in milliseconds, before terminating an http request * @param refreshTokenProducer Provide if you want to use your own logic when refreshing a Spotify token * @param onTokenRefresh Provide if you want to act on token refresh event - * @param requiredScopes Scopes that your application requires to function (only applicable to [SpotifyClientApi] and [SpotifyImplicitGrantApi]). + * @param requiredScopes Scopes that your application requires to function (only applicable to [SpotifyClientApi]). * @param proxyBaseUrl Provide if you have a proxy base URL that you would like to use instead of the Spotify API base * (https://api.spotify.com/v1). * @param retryOnInternalServerErrorTimes Whether and how often to retry once if an internal server error (500..599) has been received. Set to 0 From 81596f93fb7913c6adb4826fa17d150a8aa83937 Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 15:42:21 +0100 Subject: [PATCH 09/10] Update workflow actions to current versions --- .github/workflows/ci-client.yml | 23 ++++++--- .github/workflows/ci.yml | 44 +++++++++-------- .github/workflows/release.yml | 85 ++++++++++++++++++++++++--------- 3 files changed, 103 insertions(+), 49 deletions(-) diff --git a/.github/workflows/ci-client.yml b/.github/workflows/ci-client.yml index 263ff5f6..a5094221 100644 --- a/.github/workflows/ci-client.yml +++ b/.github/workflows/ci-client.yml @@ -19,21 +19,30 @@ jobs: environment: release steps: - name: Check out repo - uses: actions/checkout@v2 - - name: Install java 11 - uses: actions/setup-java@v2 + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' - - name: Install curl - run: sudo apt-get install -y curl libcurl4-openssl-dev + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Make gradlew executable + run: chmod +x gradlew + - name: Verify Android run: ./gradlew testDebugUnitTest + - name: Verify JVM/JS run: ./gradlew jvmTest + - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: code-coverage-report path: build/reports + if-no-files-found: warn if: always() \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 97140c98..e4fc8157 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,22 +14,28 @@ jobs: SPOTIFY_CLIENT_ID: ${{ secrets.SPOTIFY_CLIENT_ID }} SPOTIFY_CLIENT_SECRET: ${{ secrets.SPOTIFY_CLIENT_SECRET }} steps: - - name: Check out repo - uses: actions/checkout@v2 - - name: Install java 11 - uses: actions/setup-java@v2 - with: - distribution: 'adopt' - java-version: '17' - - name: Install curl - run: sudo apt-get install -y curl libcurl4-openssl-dev - - name: Test android - run: ./gradlew testDebugUnitTest - - name: Test jvm - run: ./gradlew jvmTest - - name: Archive test results - uses: actions/upload-artifact@v2 - if: always() - with: - name: code-coverage-report - path: build/reports + - name: Check out repo + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Test android + run: ./gradlew testDebugUnitTest + + - name: Test jvm + run: ./gradlew jvmTest + + - name: Archive test results + uses: actions/upload-artifact@v4 + if: always() + with: + name: code-coverage-report + path: build/reports + if-no-files-found: warn \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1a10cf2..f1cf3292 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,68 +28,107 @@ jobs: environment: release steps: - name: Check out repo - uses: actions/checkout@v2 - - name: Install java 11 - uses: actions/setup-java@v2 + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' - - name: Install curl - run: sudo apt-get install -y curl libcurl4-openssl-dev + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Make gradlew executable + run: chmod +x gradlew + - name: Verify Android run: ./gradlew testDebugUnitTest + - name: Verify JVM/JS run: ./gradlew jvmTest + - name: Publish JVM/Linux/Android run: ./gradlew publishKotlinMultiplatformPublicationToNexusRepository publishJvmPublicationToNexusRepository publishAndroidPublicationToNexusRepository publishLinuxX64PublicationToNexusRepository publishJsPublicationToNexusRepository + - name: Archive test results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: - name: code-coverage-report + name: code-coverage-report-linux path: build/reports + if-no-files-found: warn if: always() + release_mac: runs-on: macos-latest environment: release needs: release_android_jvm_linux_js steps: - name: Check out repo - uses: actions/checkout@v2 - - name: Install java 11 - uses: actions/setup-java@v2 + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Make gradlew executable + run: chmod +x gradlew + - name: Publish macOS/iOS run: ./gradlew publishMacosX64PublicationToNexusRepository publishIosX64PublicationToNexusRepository publishIosArm64PublicationToNexusRepository + release_windows: runs-on: windows-latest environment: release needs: release_android_jvm_linux_js steps: - name: Check out repo - uses: actions/checkout@v2 - - name: Install java 11 - uses: actions/setup-java@v2 + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' - - run: choco install curl + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Fix gradlew permissions + run: git update-index --chmod=+x gradlew + shell: bash + - name: Publish windows run: ./gradlew publishMingwX64PublicationToNexusRepository + shell: bash + release_docs: runs-on: ubuntu-latest environment: release steps: - name: Check out repo - uses: actions/checkout@v2 - - name: Install java 11 - uses: actions/setup-java@v2 + uses: actions/checkout@v4 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - distribution: 'adopt' + distribution: 'temurin' java-version: '17' + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + + - name: Make gradlew executable + run: chmod +x gradlew + - name: Build docs run: ./gradlew dokkaHtml + - name: Push docs to docs repo uses: cpina/github-action-push-to-another-repository@main env: @@ -98,5 +137,5 @@ jobs: source-directory: 'docs' destination-github-username: 'adamint' destination-repository-name: 'spotify-web-api-kotlin-docs' - user-email: adam@adamratzman.com - target-branch: main + user-email: 'adam@adamratzman.com' + target-branch: 'main' \ No newline at end of file From d6c24b05fea8ea7b017fbf610db43075b9dfb1ce Mon Sep 17 00:00:00 2001 From: redslime Date: Tue, 3 Mar 2026 17:06:44 +0100 Subject: [PATCH 10/10] Mark genre & previewUrls as restricted --- .../kotlin/com.adamratzman.spotify/models/Albums.kt | 2 +- .../kotlin/com.adamratzman.spotify/models/Artists.kt | 2 +- .../kotlin/com.adamratzman.spotify/models/Episode.kt | 3 ++- src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt | 4 ++-- .../kotlin/com.adamratzman/spotify/utilities/JsonTests.kt | 1 - 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Albums.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Albums.kt index ae589f01..c7710449 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Albums.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Albums.kt @@ -142,7 +142,7 @@ public data class Album( val artists: List, val copyrights: List, - val genres: List, + @SpotifyExtendedQuota val genres: List, val images: List? = null, @SpotifyExtendedQuota val label: String? = null, val name: String, diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Artists.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Artists.kt index ac158387..0c6c4220 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Artists.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Artists.kt @@ -59,7 +59,7 @@ public data class Artist( override val uri: ArtistUri, @SpotifyExtendedQuota val followers: Followers? = null, - val genres: List, + @SpotifyExtendedQuota val genres: List, val images: List? = null, val name: String? = null, @SpotifyExtendedQuota val popularity: Double? = null, diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Episode.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Episode.kt index d4c291bf..51f2cf86 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Episode.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Episode.kt @@ -3,6 +3,7 @@ package com.adamratzman.spotify.models import com.adamratzman.spotify.SpotifyRestAction import com.adamratzman.spotify.SpotifyScope +import com.adamratzman.spotify.annotations.SpotifyExtendedQuota import com.adamratzman.spotify.utils.Locale import com.adamratzman.spotify.utils.Market import kotlinx.serialization.SerialName @@ -89,7 +90,7 @@ public data class PodcastEpisodeTrack( */ @Serializable public data class Episode( - @SerialName("audio_preview_url") val audioPreviewUrl: String? = null, + @SpotifyExtendedQuota @SerialName("audio_preview_url") val audioPreviewUrl: String? = null, val description: String? = null, @SerialName("duration_ms") val durationMs: Int, val explicit: Boolean, diff --git a/src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt b/src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt index 8a7e40d6..7f3916ce 100644 --- a/src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt +++ b/src/commonMain/kotlin/com.adamratzman.spotify/models/Track.kt @@ -52,7 +52,7 @@ public data class SimpleTrack( @SerialName("is_playable") val isPlayable: Boolean = true, @SpotifyExtendedQuota @SerialName("linked_from") override val linkedTrack: LinkedTrack? = null, val name: String, - @SerialName("preview_url") val previewUrl: String? = null, + @SpotifyExtendedQuota @SerialName("preview_url") val previewUrl: String? = null, @SerialName("track_number") val trackNumber: Int, val type: String, @SerialName("is_local") val isLocal: Boolean? = null, @@ -142,7 +142,7 @@ public data class Track( @SpotifyExtendedQuota @SerialName("linked_from") override val linkedTrack: LinkedTrack? = null, val name: String, @SpotifyExtendedQuota val popularity: Double? = null, - @SerialName("preview_url") val previewUrl: String? = null, + @SpotifyExtendedQuota @SerialName("preview_url") val previewUrl: String? = null, @SerialName("track_number") val trackNumber: Int, override val type: String, @SerialName("is_local") val isLocal: Boolean? = null, diff --git a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt index 0328638e..d88cdd0e 100644 --- a/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt +++ b/src/commonTest/kotlin/com.adamratzman/spotify/utilities/JsonTests.kt @@ -112,7 +112,6 @@ class JsonTests { assertEquals(5, playlist.tracks.total) assertEquals(5, playlist.items.total) assertEquals("Api", playlist.items.items[0].item?.asTrack?.name) - println(playlist) } @Test