All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.adamratzman.spotify.SpotifyApi.kt Maven / Gradle / Ivy

/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2022; Original author: Adam Ratzman */
@file:Suppress("LeakingThis")

package com.adamratzman.spotify

import com.adamratzman.spotify.SpotifyException.BadRequestException
import com.adamratzman.spotify.endpoints.client.*
import com.adamratzman.spotify.endpoints.pub.*
import com.adamratzman.spotify.http.*
import com.adamratzman.spotify.models.AuthenticationError
import com.adamratzman.spotify.models.Token
import com.adamratzman.spotify.models.TokenValidityResponse
import com.adamratzman.spotify.models.serialization.nonstrictJson
import com.adamratzman.spotify.models.serialization.toObject
import com.adamratzman.spotify.utils.asList
import com.adamratzman.spotify.utils.base64ByteEncode
import kotlinx.serialization.json.Json
import kotlin.jvm.JvmOverloads

/**
 * Represents an instance of the Spotify API client, with common
 * functionality and information between the [SpotifyClientApi] and [SpotifyAppApi]
 * implementations of the API
 *
 * @param clientId The application client id found on the application [dashboard](https://developer.spotify.com/dashboard/applications)
 * @param clientSecret The application client secret found on the application [dashboard](https://developer.spotify.com/dashboard/applications)
 * @param token The access token associated with this API instance
 * @param spotifyApiOptions Configurable Spotify API options.
 *
 * @property search Provides access to the Spotify [search endpoint](https://developer.spotify.com/documentation/web-api/reference/search/search/)
 * @property albums Provides access to Spotify [album endpoints](https://developer.spotify.com/documentation/web-api/reference/albums/)
 * @property browse Provides access to Spotify [browse endpoints](https://developer.spotify.com/documentation/web-api/reference/browse/)
 * @property artists Provides access to Spotify [artist endpoints](https://developer.spotify.com/documentation/web-api/reference/artists/)
 * @property tracks Provides access to Spotify [track endpoints](https://developer.spotify.com/documentation/web-api/reference/tracks/)
 * @property episodes Provides access to Spotify [episode endpoints](https://developer.spotify.com/documentation/web-api/reference/episodes/)
 * @property shows Provides access to Spotify [show endpoints](https://developer.spotify.com/documentation/web-api/reference/shows/)
 * @property markets Provides access to Spotify [market endpoints](https://developer.spotify.com/documentation/web-api/reference/#category-markets)
 */
public sealed class SpotifyApi, B : ISpotifyApiBuilder>(
    public val clientId: String?,
    public val clientSecret: String?,
    public var token: Token,
    public var spotifyApiOptions: SpotifyApiOptions
) {
    public var useCache: Boolean = spotifyApiOptions.useCache
        set(value) {
            if (!value) clearCache()

            field = value
        }
    public val expireTime: Long get() = token.expiresAt
    public var runExecutableFunctions: Boolean = true

    public abstract val search: SearchApi
    public abstract val albums: AlbumApi
    public abstract val browse: BrowseApi
    public abstract val artists: ArtistApi
    public abstract val playlists: PlaylistApi
    public abstract val users: UserApi
    public abstract val tracks: TrackApi
    public abstract val following: FollowingApi
    public abstract val episodes: EpisodeApi
    public abstract val shows: ShowApi
    public abstract val markets: MarketsApi

    /**
     * Base url for Spotify web api calls
     */
    internal val spotifyApiBase = "https://api.spotify.com/v1"

    internal val defaultEndpoint get() = tracks

    init {
        spotifyApiOptions.requiredScopes?.let { requiredScopes ->
            val tokenScopes = token.scopes ?: listOf()
            if (!tokenScopes.containsAll(requiredScopes)) {
                val missingScopes = requiredScopes.filter { it !in tokenScopes }

                throw IllegalStateException(
                    "Expected authorized scopes $requiredScopes, but was missing the following scopes: $missingScopes"
                )
            }
        }
    }

    /**
     * Obtain a map of all currently-cached requests
     */
    public fun getCache(): Map =
        endpoints.map { it.cache.cachedRequests.asList() }.flatten().toMap()

    /**
     * Change the current [Token]'s access token
     */
    public fun updateTokenWith(tokenString: String) {
        updateToken {
            accessToken = tokenString
        }
    }

    /**
     * Modify the current [Token] via DSL
     */
    public fun updateToken(modifier: Token.() -> Unit) {
        modifier(token)
    }

    /**
     * A list of all endpoints included in this api type
     */
    public abstract val endpoints: List

    /**
     * If the cache is enabled, clear all stored queries in the cache
     */
    public fun clearCache(): Unit = clearCaches(*endpoints.toTypedArray())

    /**
     * Return a new [SpotifyApiBuilder] with the parameters provided to this api instance
     */
    public abstract fun getApiBuilder(): SpotifyApiBuilder

    /**
     * Return a new [B] with the parameters provided to this api instance
     */
    public abstract fun getApiBuilderDsl(): B

    private fun clearCaches(vararg endpoints: SpotifyEndpoint) {
        endpoints.forEach { it.cache.clear() }
    }

    /**
     * Create a Spotify authorization URL from which client access can be obtained
     *
     * @param scopes The scopes that the application should have access to
     * @param redirectUri The redirect uri specified on the Spotify developer dashboard; where to
     * redirect the browser after authentication
     * @param state This provides protection against attacks such as cross-site request forgery.
     *
     * @return Authorization URL that can be used in a browser
     */
    public fun getAuthorizationUrl(vararg scopes: SpotifyScope, redirectUri: String, state: String? = null): String {
        require(clientId != null)
        return getAuthUrlFull(
            *scopes,
            clientId = clientId,
            redirectUri = redirectUri,
            state = state
        )
    }

    public fun getSpotifyPkceAuthorizationUrl(
        vararg scopes: SpotifyScope,
        redirectUri: String,
        codeChallenge: String,
        state: String? = null
    ): String {
        require(clientId != null)
        return getPkceAuthUrlFull(
            *scopes,
            clientId = clientId,
            redirectUri = redirectUri,
            codeChallenge = codeChallenge,
            state = state
        )
    }

    /**
     * Tests whether the current [token] is actually valid. By default, an endpoint is called *once* to verify
     * validity.
     *
     * @param makeTestRequest Whether to also make an endpoint request to verify authentication.
     *
     * @return [TokenValidityResponse] containing whether this token is valid, and if not, an Exception explaining why
     */
    @JvmOverloads
    public suspend fun isTokenValid(
        makeTestRequest: Boolean = true
    ): TokenValidityResponse {
        if (token.shouldRefresh()) {
            return TokenValidityResponse(
                false,
                SpotifyException.AuthenticationException("Token needs to be refreshed (is it expired?)")
            )
        }
        if (!makeTestRequest) return TokenValidityResponse(true, null)

        return try {
            browse.getAvailableGenreSeeds()
            TokenValidityResponse(true, null)
        } catch (e: Exception) {
            TokenValidityResponse(false, e)
        }
    }

    /**
     * Tests whether the current [token] is actually valid. By default, an endpoint is called *once* to verify
     * validity.
     *
     * @param makeTestRequest Whether to also make an endpoint request to verify authentication.
     *
     * @return [TokenValidityResponse] containing whether this token is valid, and if not, an Exception explaining why
     */
    @JvmOverloads
    public fun isTokenValidRestAction(makeTestRequest: Boolean = true): SpotifyRestAction =
        SpotifyRestAction {
            isTokenValid(makeTestRequest)
        }

    /**
     * If the method used to create the [token] supports token refresh and
     * the information in [token] is accurate, attempt to refresh the token
     *
     * @return The old access token if refresh was successful
     * @throws BadRequestException if refresh fails
     * @throws IllegalStateException if [SpotifyApiOptions.refreshTokenProducer] is null
     */
    public suspend fun refreshToken(): Token {
        val oldToken = token
        val refreshedToken = spotifyApiOptions.refreshTokenProducer?.invoke(this)
            ?: throw SpotifyException.ReAuthenticationNeededException(IllegalStateException("The refreshTokenProducer is null."))

        token = refreshedToken
        // Spotify may not provide a new refresh token
        if (token.refreshToken == null) token.refreshToken = oldToken.refreshToken
        
        spotifyApiOptions.onTokenRefresh?.invoke(this@SpotifyApi)
        spotifyApiOptions.afterTokenRefresh?.invoke(this@SpotifyApi)
        
        return oldToken
    }

    /**
     * If the method used to create the [token] supports token refresh and
     * the information in [token] is accurate, attempt to refresh the token
     *
     * @return The old access token if refresh was successful
     * @throws BadRequestException if refresh fails
     * @throws IllegalStateException if [SpotifyApiOptions.refreshTokenProducer] is null
     */
    public fun refreshTokenRestAction(): SpotifyRestAction = SpotifyRestAction { refreshToken() }

    public companion object {
        internal suspend fun testTokenValidity(api: GenericSpotifyApi) {
            if (!api.isTokenValid().isValid) {
                try {
                    api.refreshToken()
                } catch (e: BadRequestException) {
                    throw SpotifyException.AuthenticationException(
                        "Invalid token and refresh token supplied. Cannot refresh to a fresh token.",
                        e
                    )
                }
            }
        }

        /*
            Builder tools
         */

        /**
         * Get the authorization url for the provided [clientId] and [redirectUri] application settings, when attempting to authorize with
         * specified [scopes]
         *
         * @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 state This provides protection against attacks such as cross-site request forgery.
         */
        public fun getAuthUrlFull(
            vararg scopes: SpotifyScope,
            clientId: String,
            redirectUri: String,
            isImplicitGrantFlow: Boolean = false,
            shouldShowDialog: Boolean = false,
            state: String? = null
        ): String {
            return "https://accounts.spotify.com/authorize/?client_id=$clientId" +
                    "&response_type=${if (isImplicitGrantFlow) "token" else "code"}" +
                    "&redirect_uri=$redirectUri" +
                    (state?.let { "&state=$it" } ?: "") +
                    if (scopes.isEmpty()) {
                        ""
                    } else {
                        "&scope=${scopes.joinToString("%20") { it.uri }}" +
                                if (shouldShowDialog) "&show_dialog=$shouldShowDialog" else ""
                    }
        }

        /**
         * Get the PKCE authorization url for the provided [clientId] and [redirectUri] application settings, when attempting to authorize with
         * specified [scopes]
         *
         * @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 codeChallenge The code challenge corresponding to your codeVerifier. **It is highly recommend to use
         * [getSpotifyPkceCodeChallenge] to get the code challenge from a code verifier (only available for JVM/Android).**
         * @param state This provides protection against attacks such as cross-site request forgery.
         */
        public fun getPkceAuthUrlFull(
            vararg scopes: SpotifyScope,
            clientId: String,
            redirectUri: String,
            codeChallenge: String,
            state: String? = null
        ): String {
            return "https://accounts.spotify.com/authorize/?client_id=$clientId" +
                    "&response_type=code" +
                    "&redirect_uri=$redirectUri" +
                    "&code_challenge_method=S256" +
                    "&code_challenge=$codeChallenge" +
                    (state?.let { "&state=$it" } ?: "") +
                    if (scopes.isEmpty()) "" else "&scope=${scopes.joinToString("%20") { it.uri }}"
        }

        /**
         *
         * Get an application token (can only access public methods) that can be used to instantiate a new [SpotifyAppApi]
         *
         * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param api The Spotify Api instance, or null if one doesn't exist yet
         * @param json The json instance that will deserialize the response.
         */
        public suspend fun getCredentialedToken(
            clientId: String,
            clientSecret: String,
            api: GenericSpotifyApi?,
            json: Json = api?.spotifyApiOptions?.json ?: Json.Default
        ): Token {
            val response = executeTokenRequest(
                HttpRequest(
                    "https://accounts.spotify.com/api/token",
                    HttpRequestMethod.POST,
                    mapOf("grant_type" to "client_credentials"),
                    null,
                    "application/x-www-form-urlencoded",
                    listOf(),
                    api
                ),
                clientId,
                clientSecret
            )

            if (response.responseCode / 200 == 1) return response.body.toObject(Token.serializer(), null, json)

            throw BadRequestException(response.body.toObject(AuthenticationError.serializer(), null, json))
        }

        /**
         *
         * Get an application token (can only access public methods) that can be used to instantiate a new [SpotifyAppApi]
         *
         * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
         * @param api The Spotify Api instance, or null if one doesn't exist yet
         * @param json The json instance that will deserialize the response.
         */
        public fun getCredentialedTokenRestAction(
            clientId: String,
            clientSecret: String,
            api: GenericSpotifyApi?,
            json: Json = api?.spotifyApiOptions?.json ?: Json.Default
        ): SpotifyRestAction = SpotifyRestAction { getCredentialedToken(clientId, clientSecret, api, json) }
    }
}

/**
 * An API instance created with application credentials, not through
 * client authentication
 */
public class SpotifyAppApi internal constructor(
    clientId: String?,
    clientSecret: String?,
    token: Token,
    enableDefaultTokenRefreshProducerIfNoneExists: Boolean = true,
    spotifyApiOptions: SpotifyApiOptions
) : SpotifyApi(
    clientId,
    clientSecret,
    token,
    spotifyApiOptions.apply {
        if (enableDefaultTokenRefreshProducerIfNoneExists && refreshTokenProducer == null) {
            refreshTokenProducer = defaultAppApiTokenRefreshProducer
        }
    }
) {
    override val search: SearchApi = SearchApi(this)
    override val albums: AlbumApi = AlbumApi(this)
    override val browse: BrowseApi = BrowseApi(this)
    override val artists: ArtistApi = ArtistApi(this)
    override val tracks: TrackApi = TrackApi(this)
    override val episodes: EpisodeApi = EpisodeApi(this)
    override val shows: ShowApi = ShowApi(this)
    override val markets: MarketsApi = MarketsApi(this)

    /**
     * Provides access to **public** Spotify [playlist endpoints](https://developer.spotify.com/documentation/web-api/reference/playlists/)
     */
    override val playlists: PlaylistApi = PlaylistApi(this)

    /**
     * Provides access to **public** Spotify [user information](https://developer.spotify.com/documentation/web-api/reference/users-profile/get-users-profile/)
     */
    override val users: UserApi = UserApi(this)

    /**
     * Provides access to **public** playlist [follower information](https://developer.spotify.com/documentation/web-api/reference/follow/check-user-following-playlist/)
     */
    override val following: FollowingApi = FollowingApi(this)

    override val endpoints: List
        get() = listOf(
            search,
            albums,
            browse,
            artists,
            playlists,
            users,
            tracks,
            following
        )

    override fun getApiBuilder(): SpotifyApiBuilder = SpotifyApiBuilder(
        clientId,
        clientSecret,
        null
    ).apply { useCache(useCache) }

    override fun getApiBuilderDsl(): SpotifyAppApiBuilder = spotifyAppApi {
        credentials {
            clientId = [email protected]
            clientSecret = [email protected]
        }

        useCache = [email protected]
    }

    public companion object {
        private val defaultAppApiTokenRefreshProducer: suspend (SpotifyApi<*, *>) -> Token = { api ->
            require(api.clientId != null && api.clientSecret != null) { "Either the client id or the client secret is not set" }

            getCredentialedToken(api.clientId, api.clientSecret, api, api.spotifyApiOptions.json)
        }
    }
}

/**
 * An API instance created through client authentication, with access to private information
 * managed through the scopes exposed in [token]
 */
public open class SpotifyClientApi(
    clientId: String?,
    clientSecret: String?,
    public var redirectUri: String?,
    token: Token,
    public val usesPkceAuth: Boolean,
    enableDefaultTokenRefreshProducerIfNoneExists: Boolean,
    spotifyApiOptions: SpotifyApiOptions
) : SpotifyApi(
    clientId,
    clientSecret,
    token,
    spotifyApiOptions.apply {
        if (enableDefaultTokenRefreshProducerIfNoneExists && refreshTokenProducer == null) {
            refreshTokenProducer = defaultClientApiTokenRefreshProducer
        }
    }
) {
    public constructor(
        clientId: String?,
        clientSecret: String?,
        token: Token,
        spotifyApiOptions: SpotifyApiOptions
    ) : this(
        clientId,
        clientSecret,
        null,
        token,
        false,
        false,
        spotifyApiOptions
    )

    override val albums: AlbumApi = AlbumApi(this)
    override val browse: BrowseApi = BrowseApi(this)
    override val artists: ArtistApi = ArtistApi(this)
    override val tracks: TrackApi = TrackApi(this)
    override val search: SearchApi = SearchApi(this)
    override val markets: MarketsApi = MarketsApi(this)

    override val episodes: ClientEpisodeApi = ClientEpisodeApi(this)
    override val shows: ClientShowApi = ClientShowApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/playlists/) for retrieving
     * information about a user’s playlists and for managing a user’s playlists.
     * *Superset of [PlaylistApi]*
     */
    override val playlists: ClientPlaylistApi = ClientPlaylistApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/users-profile/) for
     * retrieving information about a user’s profile.
     * *Superset of [UserApi]*
     */
    override val users: ClientProfileApi = ClientProfileApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/follow/) for managing
     * the artists, users, and playlists that a Spotify user follows.
     * *Superset of [FollowingApi]*
     */
    override val following: ClientFollowingApi = ClientFollowingApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/personalization/) for
     * retrieving information about the user’s listening habits.

     */
    public val personalization: ClientPersonalizationApi = ClientPersonalizationApi(this)

    /**
     * Provides access to [endpoints](https://developer.spotify.com/documentation/web-api/reference/library/) for
     * retrieving information about, and managing, tracks that the current user has saved in their “Your Music” library.
     */
    public val library: ClientLibraryApi = ClientLibraryApi(this)

    /**
     * Provides access to the **beta** [player api](https://developer.spotify.com/documentation/web-api/reference/player/),
     * including track playing and pausing endpoints.
     *
     * Please consult the [usage guide](https://developer.spotify.com/documentation/web-api/guides/using-connect-web-api/) before
     * calling any endpoint in this api.
     *
     * **These endpoints may break at any time.**
     */
    public val player: ClientPlayerApi = ClientPlayerApi(this)

    private var userIdBacking: String? = null

    private suspend fun initiatizeUserIdBacking(): String {
        userIdBacking = users.getClientProfile().id
        return userIdBacking!!
    }

    /**
     * The Spotify user id to which the api instance is connected
     */
    public suspend fun getUserId(): String =
        if (userIdBacking != null) userIdBacking!! else initiatizeUserIdBacking()

    /**
     * The Spotify user id to which the api instance is connected
     */
    public fun getUserIdRestAction(): SpotifyRestAction = SpotifyRestAction { getUserId() }

    /**
     * Stop all automatic functions like refreshToken or clearCache and shut down the scheduled
     * executor
     * */
    public fun shutdown() {
        runExecutableFunctions = false
    }

    override val endpoints: List
        get() = listOf(
            search,
            albums,
            browse,
            artists,
            playlists,
            users,
            tracks,
            following,
            personalization,
            library,
            player
        )

    override fun getApiBuilder(): SpotifyApiBuilder = SpotifyApiBuilder(
        clientId,
        clientSecret,
        redirectUri
    ).apply {
        redirectUri(redirectUri)
        useCache(useCache)
    }

    override fun getApiBuilderDsl(): SpotifyClientApiBuilder = spotifyClientApi {
        credentials {
            clientId = [email protected]
            clientSecret = [email protected]
            redirectUri = [email protected]
        }

        useCache = [email protected]
    }

    /**
     * Create a Spotify authorization URL from which client access can be obtained
     *
     * @param scopes The scopes that the application should have access to
     *
     * @return Authorization URL that can be used in a browser
     */
    public fun getAuthorizationUrl(vararg scopes: SpotifyScope, state: String? = null): String {
        require(clientId != null && clientSecret != null) { "Either the client id or the client secret is not set" }
        return redirectUri?.let { getAuthUrlFull(*scopes, clientId = clientId, redirectUri = it, state = state) }
            ?: throw IllegalArgumentException("The redirect uri must be set")
    }

    /**
     * Whether the current access token allows access to scope [scope]
     */
    public suspend fun hasScope(scope: SpotifyScope): Boolean? = hasScopes(scope)

    /**
     * Whether the current access token allows access to scope [scope]
     */
    public fun hasScopeRestAction(scope: SpotifyScope): SpotifyRestAction =
        SpotifyRestAction { hasScope(scope) }

    /**
     * Whether the current access token allows access to all of the provided scopes
     */
    public suspend fun hasScopes(scope: SpotifyScope, vararg scopes: SpotifyScope): Boolean? =
        if (token.scopes == null) {
            null
        } else {
            isTokenValid(false).isValid &&
                    token.scopes?.contains(scope) == true &&
                    scopes.all { token.scopes?.contains(it) == true }
        }

    /**
     * Whether the current access token allows access to all of the provided scopes
     */
    public fun hasScopesRestAction(scope: SpotifyScope, vararg scopes: SpotifyScope): SpotifyRestAction =
        SpotifyRestAction {
            hasScopes(scope, *scopes)
        }

    public companion object {
        private val defaultClientApiTokenRefreshProducer: suspend (GenericSpotifyApi) -> Token = { api ->
            api as SpotifyClientApi

            require(api.clientId != null) { "The client id is not set" }

            refreshSpotifyClientToken(api.clientId, api.clientSecret, api.token.refreshToken, api.usesPkceAuth)
        }
    }
}

/**
 * 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.
 */
public class SpotifyImplicitGrantApi(
    clientId: String?,
    token: Token,
    spotifyApiOptions: SpotifyApiOptions
) : SpotifyClientApi(
    clientId,
    null,
    token,
    spotifyApiOptions
)

/**
 * Represents a generic instance of the Spotify API client, with common functionality and information between
 * implementations of the API
 */
public typealias GenericSpotifyApi = SpotifyApi<*, *>

/**
 *
 * Get an application token (can only access public methods) that can be used to instantiate a new [SpotifyAppApi]
 *
 * @param clientId Spotify [client id](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param clientSecret Spotify [client secret](https://developer.spotify.com/documentation/general/guides/app-settings/)
 * @param api The Spotify Api instance, or null if one doesn't exist yet
 * @param json The json instance that will deserialize the response.
 */
@Deprecated("Moved", ReplaceWith("SpotifyApi.getCredentialedToken"))
public suspend fun getCredentialedToken(
    clientId: String,
    clientSecret: String,
    api: GenericSpotifyApi?,
    json: Json = api?.spotifyApiOptions?.json ?: Json.Default
): Token = SpotifyApi.getCredentialedToken(clientId, clientSecret, api, json)

internal suspend fun executeTokenRequest(
    httpRequest: HttpRequest,
    clientId: String,
    clientSecret: String
): HttpResponse {
    return httpRequest.execute(
        listOf(
            HttpHeader(
                "Authorization",
                "Basic ${"$clientId:$clientSecret".base64ByteEncode()}"
            )
        )
    )
}

/**
 * Refresh a Spotify client token
 *
 * @param clientId The Spotify application client id.
 * @param clientSecret The Spotify application client secret (not needed for PKCE).
 * @param refreshToken The refresh token.
 * @param usesPkceAuth Whether this token was created using PKCE auth or not.
 */
public suspend fun refreshSpotifyClientToken(
    clientId: String,
    clientSecret: String?,
    refreshToken: String?,
    usesPkceAuth: Boolean
): Token {
    fun getDefaultClientApiTokenBody(): Map {
        val map = mutableMapOf(
            "grant_type" to "refresh_token",
            "refresh_token" to refreshToken
        )

        if (usesPkceAuth) map += "client_id" to clientId

        return map
    }

    val response = if (!usesPkceAuth) {
        require(clientSecret != null) { "The client secret is not set" }
        executeTokenRequest(
            HttpRequest(
                "https://accounts.spotify.com/api/token",
                HttpRequestMethod.POST,
                getDefaultClientApiTokenBody(),
                null,
                "application/x-www-form-urlencoded",
                listOf(),
                null
            ),
            clientId,
            clientSecret
        )
    } else {
        HttpRequest(
            "https://accounts.spotify.com/api/token",
            HttpRequestMethod.POST,
            getDefaultClientApiTokenBody(),
            null,
            "application/x-www-form-urlencoded",
            listOf(),
            null
        ).execute()
    }

    return if (response.responseCode in 200..399) {
        response.body.toObject(Token.serializer(), null, nonstrictJson)
    } else {
        throw BadRequestException(
            response.body.toObject(
                AuthenticationError.serializer(),
                null,
                nonstrictJson
            )
        )
    }
}

/**
 * Refresh a Spotify client token
 *
 * @param clientId The Spotify application client id.
 * @param clientSecret The Spotify application client secret (not needed for PKCE).
 * @param refreshToken The refresh token.
 * @param usesPkceAuth Whether this token was created using PKCE auth or not.
 */
public fun refreshSpotifyClientTokenRestAction(
    clientId: String,
    clientSecret: String?,
    refreshToken: String?,
    usesPkceAuth: Boolean
): SpotifyRestAction =
    SpotifyRestAction { refreshSpotifyClientToken(clientId, clientSecret, refreshToken, usesPkceAuth) }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy