Giter Club home page Giter Club logo

adamint / spotify-web-api-kotlin Goto Github PK

View Code? Open in Web Editor NEW
181.0 7.0 21.0 7.13 MB

Spotify Web API wrapper for Kotlin, Java, JS, and Native - Targets JVM, Android, JS (browser), Native (Desktop), and Apple tvOS/iOS. Includes a Spotify Web Playback SDK wrapper for Kotlin/JS, and a spotify-auth wrapper for Kotlin/Android.

Home Page: https://adamint.github.io/spotify-web-api-kotlin-docs/

License: MIT License

Kotlin 99.92% JavaScript 0.02% Shell 0.06%
spotify spotify-api kotlin spotify-web-api kotlin-multiplatform android android-library java javascript jvm

spotify-web-api-kotlin's Introduction

Kotlin Spotify Web API

A Kotlin implementation of the Spotify Web API, supporting Kotlin/JS, Kotlin/Android, Kotlin/JVM, and Kotlin/Native (macOS, Windows, Linux).

This library has first-class support for Java and is a viable alternative for Java development. Please see the Java section for more details.

Use this library in Kotlin, Java, JavaScript, Swift, or native code! Because this library targets both iOS and Android, it can also be used in KMM (Kotlin Multiplatform Mobile) applications as a shared source.

Maven CEntral codebeat badge

Table of Contents

Overview and how to install

Current version:

Maven Central

JVM, Android, JS, Native, Apple

repositories {
    mavenCentral()
}

implementation("com.adamratzman:spotify-api-kotlin-core:VERSION")

JS

Please see the JS Spotify Web Playback SDK wrapper to learn how to use Spotify's web playback SDK in a browser application.

Android

Note: For information on how to integrate implicit/PKCE authentication, Spotify app remote, and Spotify broadcast notifications into your application, please see the Android README.

If you declare any release types not named debug or release, you may see "Could not resolve com.adamratzman:spotify-api-kotlin-android:VERSION". You need to do the following for each release type not named debug or release:

android {
    buildTypes {
        yourReleaseType1 {
            // ...
            matchingFallbacks = ['release', 'debug'] 
        }
        yourReleaseType2 {
            // ...
            matchingFallbacks = ['release', 'debug'] 
        }
	...
    }
}

To successfully build, you might need to exclude kotlin_module files from the packaging. To do this, inside the android/buildTypes/release closure, you would put:

packagingOptions {
	exclude 'META-INF/*.kotlin_module'
}

Android sample application

You can find a simple sample application demonstrating how to use spotify-web-api-kotlin in a modern Android app, as well as how to integrate with the Spotify app, here.

Documentation

The spotify-web-api-kotlin JavaDocs are hosted here.

Have a question?

If you have a question, you can:

  1. Create an issue
  2. Join our Discord server
  3. Contact me using Adam#9261 on Discord

Unsupported features on each platform:

Feature JVM Android JS Native (Mac/Windows/Linux)
Edit client playlist ✔️ ✔️ ✔️ Unsupported
Remove playlist tracks ✔️ ✔️ ✔️ Unsupported

Please feel free to open an issue/discussion on GitHub or Discord if you need access to one of these features or have an interest in implementing one, as direction can be provided.

Creating a new api instance

To decide which api you need (SpotifyAppApi, SpotifyClientApi, SpotifyImplicitGrantApi), you can refer to the sections below or the Spotify authorization guide. In general:

  • If you don't need client resources, use SpotifyAppApi
  • If you're using the api in a backend application, use SpotifyClientApi (with or without PKCE)
  • If you're using the api in Kotlin/JS browser, use SpotifyImplicitGrantApi
  • If you need access to client resources in an Android or other application, use SpotifyClientApi with PKCE

Note: You can use the online Spotify OAuth Token Generator tool to generate a client token for local testing.

SpotifyAppApi

This provides access only to public Spotify endpoints. Use this when you have a server-side application. Note that implicit grant authorization provides a higher api ratelimit, so consider using implicit grant if your application has significant usage.

By default, the SpotifyApi Token automatically regenerates when needed. This can be changed by overriding the automaticRefresh builder setting.

There are four exposed builders, depending on the level of control you need over api creation. Please see the spotifyAppApi builder docs for a full list of available builders.

You will need:

  • Spotify application client id
  • Spotify application client secret

Example creation (default settings)

val api = spotifyAppApi("clientId", "clientSecret").build() // create and build api
println(api.browse.getNewReleases()) // use it

Example creation, using an existing Token and setting automatic token refresh to false

val token = spotifyAppApi(spotifyClientId, spotifyClientSecret).build().token
val api = spotifyAppApi(
  "clientId",
  "clientSecret",
  token
) { 
  automaticRefresh = false 
}.build()

println(api.browse.getNewReleases()) // use it

SpotifyClientApi

The SpotifyClientApi is a superset of SpotifyApi; thus, nothing changes if you want to access public data. This library does not provide a method to retrieve the code from your callback url; instead, you must implement that with a web server. Automatic Token refresh is available only when building with an authorization code or a Token object. Otherwise, it will expire Token.expiresIn seconds after creation.

Make sure your application has requested the proper Scopes in order to ensure proper function of this library. The api option requiredScopes allows you to verify that a client has actually authorized with the scopes you are expecting.

You will need:

  • Spotify application client id
  • Spotify application client secret (if not using PKCE)
  • Spotify application redirect uri
  • To choose which client authorization method (PKCE or non-PKCE) to use

PKCE

Use the PKCE builders and helper methods if you are using the Spotify client authorization PKCE flow. Building via PKCE returns a SpotifyClientApi which has modified refresh logic.

Use cases:

  1. You are using this library in an application (likely Android), or do not want to expose the client secret.

To learn more about the PKCE flow, please read the Spotify authorization guide. Some highlights about the flow are:

  • It is refreshable, but each refresh token can only be used once. This library handles token refresh automatically by default
  • It does not require a client secret; instead, a set redirect uri and a random code verifier are used to verify the authenticity of the authorization.
  • A code verifier is required. The code verifier is "a cryptographically random string between 43 and 128 characters in length. It can contain letters, digits, underscores, periods, hyphens, or tildes."
  • A code challenge is required. "In order to generate the code challenge, your app should hash the code verifier using the SHA256 algorithm. Then, base64url encode the hash that you generated."
  • When creating a pkce api instance, the code verifier is passed in by you and compared to the code challenge used to authorize the user.

This library contains helpful methods that can be used to simplify the PKCE authorization process. This includes getSpotifyPkceCodeChallenge, which SHA256 hashes and base64url encodes the code challenge, and getSpotifyPkceAuthorizationUrl, which allows you to generate an easy authorization url for PKCE flow.

Please see the spotifyClientPkceApi builder docs for a full list of available builders.

Takeaway: Use PKCE authorization flow in applications where you cannot secure the client secret.

To get a PKCE authorization url, to which you can redirect a user, you can use the getSpotifyPkceAuthorizationUrl top-level method. An example is shown below, requesting 4 different scopes.

val codeVerifier = "thisisaveryrandomalphanumericcodeverifierandisgreaterthan43characters"
val codeChallenge = getSpotifyPkceCodeChallenge(codeVerifier) // helper method
val url: String = getSpotifyPkceAuthorizationUrl(
    SpotifyScope.PLAYLIST_READ_PRIVATE,
    SpotifyScope.PLAYLIST_MODIFY_PRIVATE,
    SpotifyScope.USER_FOLLOW_READ,
    SpotifyScope.USER_LIBRARY_MODIFY,
    clientId = "clientId",
    redirectUri = "your-redirect-uri",
    codeChallenge = codeChallenge
)

There is also an optional parameter state, which helps you verify the authorization.

Note: If you want automatic token refresh, you need to pass in your application client id and redirect uri when using the spotifyClientPkceApi.

Example: A user has authorized your application. You now have the authorization code obtained after the user was redirected back to your application. You want to create a new SpotifyClientApi.
val codeVerifier = "thisisaveryrandomalphanumericcodeverifierandisgreaterthan43characters"
val code: String = ...
val api = spotifyClientPkceApi(
    "clientId", // optional. include for token refresh
    "your-redirect-uri", // optional. include for token refresh
    code,
    codeVerifier // the same code verifier you used to generate the code challenge
) {
  retryWhenRateLimited = false
}.build()

println(api.library.getSavedTracks().take(10).filterNotNull().map { it.track.name })

Non-PKCE (backend applications, requires client secret)

To get a non-PKCE authorization url, to which you can redirect a user, you can use the getSpotifyAuthorizationUrl top-level method. An example is shown below, requesting 4 different scopes.

val url: String = getSpotifyAuthorizationUrl(
    SpotifyScope.PLAYLIST_READ_PRIVATE,
    SpotifyScope.PLAYLIST_MODIFY_PRIVATE,
    SpotifyScope.USER_FOLLOW_READ,
    SpotifyScope.USER_LIBRARY_MODIFY,
    clientId = "clientId",
    redirectUri = "your-redirect-uri",
    state = "your-special-state" // optional
)

There are also several optional parameters, allowing you to set whether the authorization url is meant for implicit grant flow, the state, and whether a re-authorization dialog should be shown to users.

There are several exposed builders, depending on the level of control you need over api creation. Please see the spotifyClientApi builder docs for a full list of available builders.

Example: You've redirected the user back to your web server and have an authorization code (code).

In this example, automatic token refresh is turned on by default.

val authCode = ""
val api = spotifyClientApi(
    "clientId",
    "clientSecret",
    "your-redirect-uri",
    authCode
).build() // create and build api
println(api.personalization.getTopTracks(limit = 5).items.map { it.name }) // print user top tracks
Example: You've saved a user's token from previous authorization and need to create an api instance.

In this case, if you provide a client id to the builder, automatic token refresh will also be turned on.

val token: Token = ... // your existing token
val api = spotifyClientApi(
    "clientId",
    "clientSecret",
    "your-redirect-uri",
    token
) {
  onTokenRefresh = {
    println("Token refreshed at ${System.currentTimeMillis()}")
  }
}.build()
println(api.personalization.getTopTracks(limit = 5).items.map { it.name })

SpotifyImplicitGrantApi

Use the SpotifyImplicitGrantApi if you are using the Spotify implicit grant flow. SpotifyImplicitGrantApi is a superset of SpotifyClientApi. Unlike the other builders, the spotifyImplicitGrantApi builder method directly returns a SpotifyImplicitGrantApi instead of an api builder.

Use cases:

  1. You are using the Kotlin/JS target for this library.
  2. Your frontend Javascript passes the token received through the implicit grant flow to your backend, where it is then used to create an api instance.

To learn more about the implicit grant flow, please read the Spotify authorization guide. Some highlights about the flow are:

  • It is non-refreshable
  • It is client-side
  • It does not require a client secret

Please see the spotifyImplicitGrantApi builder docs for a full list of available builders.

The Kotlin/JS target contains the parseSpotifyCallbackHashToToken method, which will parse the hash for the current url into a Token object, with which you can then instantiate the api.

Takeaway: There are two ways to use implicit grant flow, browser-side only and browser and server. This library provides easy access for both.

Example
val token: Token = ...
val api = spotifyImplicitGrantApi(
    null,
    null,
    token
) // create api. there is no need to build it 
println(api.personalization.getTopArtists(limit = 1)[0].name) // use it

SpotifyApiBuilder Block & setting API options

There are three pluggable blocks in each api's corresponding builder

  1. credentials lets you set the client id, client secret, and redirect uri
  2. authorization lets you set the type of api authorization you are using. Acceptable types include: an authorization code, a Token object, a Token's access code string, and an optional refresh token string
  3. options lets you configure API options to your own specific needs

API options

This library does not attempt to be prescriptivist. All API options are located in SpotifyApiOptions and their default values can be overridden; however, use caution in doing so, as most of the default values either allow for significant performance or feature enhancements to the API instance.

  • useCache: Set whether to cache requests. Default: true
  • cacheLimit: The maximum amount of cached requests allowed at one time. Null means no limit. Default: 200
  • automaticRefresh: Enable or disable automatic refresh of the Spotify access token when it expires. Default: true
  • retryWhenRateLimited: Set whether to block the current thread and wait until the API can retry the request. Default: true
  • enableLogger: Set whether to enable to the exception logger. Default: true
  • testTokenValidity: After API creation, test whether the token is valid by performing a lightweight request. Default: false
  • defaultLimit: The default amount of objects to retrieve in one request. Default: 50
  • json: The Json serializer/deserializer instance.
  • allowBulkRequests: Allow splitting too-large requests into smaller, allowable api requests. Default: true
  • requestTimeoutMillis: The maximum time, in milliseconds, before terminating an http request. Default: 100000ms
  • refreshTokenProducer: Provide if you want to use your own logic when refreshing a Spotify token.
  • requiredScopes: Scopes that your application requires to function (only applicable to SpotifyClientApi and SpotifyImplicitGrantApi). This verifies that the token your user authorized with actually contains the scopes your application needs to function.

Notes:

  • Unless you have a good reason otherwise, useCache should be true
  • cacheLimit is per Endpoint, not per API. Don't be surprised if you end up with over 200 items in your cache with the default settings.
  • automaticRefresh is disabled when client secret is not provided, or if tokenString is provided in SpotifyClientApi
  • allowBulkRequests for example, lets you query 80 artists in one wrapper call by splitting it into 50 artists + 30 artists
  • refreshTokenProducer is useful when you want to re-authorize with the Spotify Auth SDK or elsewhere

Using the API

APIs available in all SpotifyApi instances, including SpotifyClientApi and SpotifyImplicitGrantApi:

  • SearchApi (searching items)
  • AlbumApi (get information about albums)
  • BrowseApi (browse new releases, featured playlists, categories, and recommendations)
  • ArtistApi (get information about artists)
  • PlaylistApi (get information about playlists)
  • UserApi (get public information about users on Spotify)
  • TrackApi (get information about tracks)
  • FollowingApi (check whether users follow playlists)

APIs available only in SpotifyClientApi and SpotifyImplicitGrantApi instances:

  • ClientSearchApi (all the methods in SearchApi, and searching shows and episodes)
  • EpisodeApi (get information about episodes)
  • ShowApi (get information about shows)
  • ClientPlaylistApi (all the methods in PlaylistApi, and get and manage user playlists)
  • ClientProfileApi (all the methods in UserApi, and get the user profile, depending on scopes)
  • ClientFollowingApi (all the methods in FollowingApi, and get and manage following of playlists, artists, and users)
  • ClientPersonalizationApi (get user top tracks and artists)
  • ClientLibraryApi (get and manage saved tracks and albums)
  • ClientPlayerApi (view and control Spotify playback)

Platform-specific wrappers and information

Java

This library has first-class support for Java! You have two choices when using this library: async-only with Kotlin suspend functions (using SpotifyContinuation).

Integrating with Kotlin suspend functions via Java Continuations

Unfortunately, coroutines don't play very nicely with Java code. Fortunately, however, we provide a wrapper around Kotlin's Continuation class that allows you to directly implement onSuccess and onFailure handlers on API methods.

Please see below for an example:

import com.adamratzman.spotify.SpotifyApiBuilderKt;
import com.adamratzman.spotify.SpotifyAppApi;
import com.adamratzman.spotify.javainterop.SpotifyContinuation;
import com.adamratzman.spotify.models.Album;
import org.jetbrains.annotations.NotNull;

import java.util.concurrent.ExecutionException;

public class SpotifyTestApp {
    static SpotifyAppApi api;

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        var id = "spotify-client-id";
        var secret = "spotify-client-secret";
        SpotifyApiBuilderKt.spotifyAppApi(id, secret).build(true, new SpotifyContinuation<>() {
            @Override
            public void onSuccess(SpotifyAppApi spotifyAppApi) {
                api = spotifyAppApi;
                runAlbumSearch();
            }

            @Override
            public void onFailure(@NotNull Throwable throwable) {
                throwable.printStackTrace();
            }
        });

        Thread.sleep(1000000);
    }

    public static void runAlbumSearch() {
        api.getAlbums().getAlbum("spotify:album:0b23AHutIA1BOW0u1dZ6wM", null, new SpotifyContinuation<>() {
            @Override
            public void onSuccess(Album album) {
                System.out.println("Album name is: " + album.getName() + ". Exiting now..");

                System.exit(0);
            }

            @Override
            public void onFailure(@NotNull Throwable throwable) {
                throwable.printStackTrace();
            }
        });
    }
}

Android authentication

For information on how to integrate implicit/PKCE authentication, Spotify app remote, and Spotify broadcast notifications into your application, please see the Android README.

JS Spotify Web Playback SDK wrapper

spotify-web-api-kotlin provides a wrapper around Spotify's Web Playback SDK for playing music via Spotify in the browser on your own site.

To do this, you need to create a Player instance and then use the associated methods to register listeners, play, and get current context.

Please see an example of how to do this here. An example project, spotify-web-api-browser-example, demonstrates how to create a frontend JS Kotlin application with Spotify integration and that will play music in the browser.

Notes:

  1. You must include the Spotify player JS script by including <script src="https://sdk.scdn.co/spotify-player.js"></script>
  2. You must define a window.onSpotifyWebPlaybackSDKReady function immediately afterwards - this should load your main application bundle. Otherwise, you will get errors. An example is below:
<html>
<head>
    ...
    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>

    <script>
        jQuery.loadScript = function (url, callback) {
            jQuery.ajax({
                url: url,
                dataType: 'script',
                success: callback,
                async: true
            });
        }
    </script>

    <script src="https://sdk.scdn.co/spotify-player.js"></script>
    <script>
        window.onSpotifyWebPlaybackSDKReady = () => {
            $.loadScript("main.bundle.js")
        }
    </script>
</head>
<body>
....
</body>
</html>

Tips

Building the API

The easiest way to build the API is using .build() after a builder

runBlocking {
    val api = spotifyAppApi(clientId, clientSecret).build()
}

Notes

Re-authentication

If you are using an authorization flow or token that does not support automatic token refresh, SpotifyException.ReAuthenticationNeededException will be thrown. You should put your requests, if creating an application, behind a try/catch block to re-authenticate users if this exception is thrown.

LinkedResults, PagingObjects, and Cursor-based Paging Objects

Spotify provides these three object models in order to simplify our lives as developers. So let's see what we can do with them!

PagingObjects

PagingObjects are a container for the requested objects (items), but also include important information useful in future calls. It contains the request's limit and offset, along with (sometimes) a link to the next and last page of items and the total number of items returned.

If a link to the next or previous page is provided, we can use the getNext and getPrevious methods to retrieve the respective PagingObjects

Cursor-Based Paging Objects

A cursor-based paging object is a PagingObject with a cursor added on that can be used as a key to find the next page of items. The value in the cursor, after, describes after what object to begin the query.

Just like with PagingObjects, you can get the next page of items with getNext. However, there is no provided implementation of after in this library. You will need to do it yourself, if necessary.

LinkedResults

Some endpoints, like PlaylistAPI.getPlaylistTracks, return a LinkedResult, which is a simple wrapper around the list of objects. With this, we have access to its Spotify API url (with href), and we provide simple methods to parse that url.

Generic Request

For obvious reasons, in most cases, making asynchronous requests via queue or queueAfter is preferred. However, the synchronous format is also shown.

val api = spotifyAppApi(
        System.getenv("SPOTIFY_CLIENT_ID"),
        System.getenv("SPOTIFY_CLIENT_SECRET")
).build()

// print out the names of the twenty most similar songs to the search
println(api.search.searchTrack("Début de la Suite").joinToString { it.name })

// simple, right? what about if we want to print out the featured playlists message from the "Overview" tab?
println(api.browse.getFeaturedPlaylists().message)

// easy! let's try something a little harder
// let's find out Bénabar's Spotify ID, find his top tracks, and print them out
val benabarId = api.search.searchArtist("Bénabar")[0].id
// this works; a redundant way would be: api.artists.getArtist("spotify:artist:6xoAWsIOZxJVPpo7Qvqaqv").id
println(api.artists.getArtistTopTracks(benabarId).joinToString { it.name })

Track Relinking

Spotify keeps many instances of most tracks on their servers, available in different markets. As such, if we use endpoints that return tracks, we do not know if these tracks are playable in our market. That's where track relinking comes in.

To relink in a specified market, we must supply a market parameter for endpoints where available. In both Track and SimpleTrack objects in an endpoint response, there is a nullable field called linked_from. If the track is unable to be played in the specified market and there is an alternative that is playable, this will be populated with the href, uri, and, most importantly, the id of the track.

You can then use this track in SpotifyClientApi endpoints such as playing or saving the track, knowing that it will be playable in your market!

Contributing

See CONTRIBUTING.md

spotify-web-api-kotlin's People

Contributors

adamint avatar bobbyesp avatar carlos-menezes avatar code-factor avatar dependabot-support avatar hufman avatar itwasender avatar molikuner avatar nielssg avatar shellwen avatar xaseron avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

spotify-web-api-kotlin's Issues

Documentation & Wiki

Document methods with information obtained from Spotify, and create a detailed Wiki documenting how to use the libraries

switch to a faster JSON parser

I dislike Gson's performance and many of the applications that can implement this library will already be using Jackson. We actually have a few good options, mainly

  1. Use Jackson
  2. Manually map each JSONObject to its respective class

Http proxy

Is it possible to use a http proxy for requests?

rest in peace Jsoup 2018-2018

I see no obvious way (whether using Jsoup itself or extension functions) to allow us to send DELETE requests with a JSON body. That’s the issue we’ve been having with playlist track deletion.

There’s no point having Jsoup if we have to fall back on HttpsUrlConnection for any http methods.

I propos abstracting requests into our own class (SimpleHttps) and SimpleHttpsBuilder DSL. Provide convenience getters for header(s), request body, status code, and request time taken

Dependabot couldn't find a build.gradle for this project

Dependabot couldn't find a build.gradle for this project.

Dependabot requires a build.gradle to evaluate your project's current Java dependencies. It had expected to find one at the path: /build.gradle.

If this isn't a Java project, or if it is a library, you may wish to disable updates for it from within Dependabot.

Cannot build SpotifyClientAPI with expired token

Description
If you create an instance with an expired token an exception gets thrown in the init method. This should not be, because you aren't even able to refresh your access token with a valid refresh token.
This is because the user profile get loaded in the init function, which throws the exception.

To Reproduce
Build an instance of the SpotifyClientAPI with an expired token.

Stack

com.adamratzman.spotify.models.BadRequestException: Received Status Code 401. Error cause: Invalid access token
	at com.adamratzman.spotify.http.SpotifyEndpoint.handleResponse(Endpoints.kt:110) ~[spotify-api-kotlin-2.3.08.jar:na]
	at com.adamratzman.spotify.http.SpotifyEndpoint.execute(Endpoints.kt:70) ~[spotify-api-kotlin-2.3.08.jar:na]
	at com.adamratzman.spotify.http.SpotifyEndpoint.execute$default(Endpoints.kt:49) ~[spotify-api-kotlin-2.3.08.jar:na]
	at com.adamratzman.spotify.http.SpotifyEndpoint.get$spotify_api_kotlin(Endpoints.kt:25) ~[spotify-api-kotlin-2.3.08.jar:na]
	at com.adamratzman.spotify.endpoints.client.ClientUserAPI$getUserProfile$1.get(ClientUserAPI.kt:29) ~[spotify-api-kotlin-2.3.08.jar:na]
	at com.adamratzman.spotify.endpoints.client.ClientUserAPI$getUserProfile$1.get(ClientUserAPI.kt:16) ~[spotify-api-kotlin-2.3.08.jar:na]
	at com.adamratzman.spotify.SpotifyRestAction.complete(SpotifyRestAction.kt:34) ~[spotify-api-kotlin-2.3.08.jar:na]
	at com.adamratzman.spotify.SpotifyClientAPI.<init>(SpotifyAPI.kt:354) ~[spotify-api-kotlin-2.3.08.jar:na]
	at com.adamratzman.spotify.SpotifyApiBuilderDsl.buildClient(Builder.kt:460) ~[spotify-api-kotlin-2.3.08.jar:na]
	at com.adamratzman.spotify.SpotifyApiBuilderDsl.buildClient(Builder.kt:400) ~[spotify-api-kotlin-2.3.08.jar:na]

Coroutine support for SpotifyRestAction

We could expose a way to use the capabilities of coroutines, as they are kind of a language feature.

Possible implementation:

  • wrapping the callback method of SpotifyRestAction to suspend the coroutine

Allow for creation of a SpotifyClientAPI using a refresh token

Hi there! I'd like to be able to write my own code to fetch a refresh token from Spotify, then save that refresh token somewhere persistent, then have my app create a SpotifyClientAPI using that refresh token. Is there a way to do that?

I know that if I provide an auth code, this library will fetch its own refresh token. But if I rely on that, I need to go through the authentication process more or less every time I restart my app (unless my last restart was so recent that the old auth code is still valid). It'd be better if I could provide this library a refresh token manually.

(I'm new to Kotlin, by the way, so it's possible that there's a totally-obvious way to do this. I didn't see it in the main README, and I wasn't able to figure it out by looking around in SpotifyAPI.kt.)

Thanks for making this library!

Dependabot couldn't find a build.gradle for this project

Dependabot couldn't find a build.gradle for this project.

Dependabot requires a build.gradle to evaluate your project's current Java dependencies. It had expected to find one at the path: /build.gradle.

If this isn't a Java project, or if it is a library, you may wish to disable updates for it from within Dependabot.

Dependabot couldn't find a build.gradle for this project

Dependabot couldn't find a build.gradle for this project.

Dependabot requires a build.gradle to evaluate your project's current Java dependencies. It had expected to find one at the path: /build.gradle.

If this isn't a Java project, or if it is a library, you may wish to disable updates for it from within Dependabot.

Automatic token refresh fails if token is already expired

When using an already expired token the library tries to schedule for -30s. That isn't allowed for obvious reasons.

Stack:

Exception in thread "main" java.lang.IllegalArgumentException
	at java.util.concurrent.ScheduledThreadPoolExecutor.scheduleAtFixedRate(ScheduledThreadPoolExecutor.java:565)
	at com.adamratzman.spotify.main.SpotifyClientAPI.init(SpotifyAPI.kt:247)
	at com.adamratzman.spotify.main.SpotifyClientAPI.<init>(SpotifyAPI.kt:241)
	at com.adamratzman.spotify.main.SpotifyApiBuilder.buildClient(SpotifyAPI.kt:173)
	at com.adamratzman.spotify.main.SpotifyApiBuilder.buildClient(SpotifyAPI.kt:141)
	at com.molikuner.MainKt.main(Main.kt:28)

Could be fixed by refreshing the token before setting this schedule up.

Tracks (in a playlist) are sometimes null, parsing exception is thrown

Describe the bug
Recently I found some playlists with an extra track which is null. As this is against the docs, I would not allow the library to parse this, but feel free to start a discussion about it.
Current example (may get invalid in future, as this is a public playlist...): https://api.spotify.com/v1/playlists/37i9dQZF1DXc5e2bJhV6pu -> track was null at response.tracks.items[50].track

Error thrown by library:

com.adamratzman.spotify.SpotifyException: Unable to parse {
  "collaborative" : false,
  "description" : "Get your day off to a cracking start!",
  "external_urls" : {
    "spotify" : "https://open.spotify.com/playlist/37i9dQZF1DXc5e2bJhV6pu"
  },
  "followers" : {
    "href" : null,
    "total" : 615993
  },
  "href" : "https://api.spotify.com/v1/playlists/37i9dQZF1DXc5e2bJhV6pu",
  "id" : "37i9dQZF1DXc5e2bJhV6pu",
  "images" : [ {
    "height" : null,
    "url" : "https://pl.scdn.co/images/pl/default/3297e1b50ff1c49e88c28eb07627a98d1cbb3bbb",
    "width" : null
  } ],
  "name" : "Morning Motivation",
  "owner" : {
    "display_name" : "Spotify",
    "external_urls" : {
      "spotify" : "https://open.spotify.com/user/spotify"
    },
    "href" : "https://api.spotify.com/v1/users/spotify",
    "id" : "spotify",
    "type" : "user",
    "uri" : "spotify:user:spotify"
  },
  "primary_color" : null,
  "public" : false,
  "snapshot_id" : "MTU2Njk0OTM4MCwwMDAwMDAwMDAyZDNkZDhlZmZkZmQ5MjUxMWFlOWM4NWNlZWMxYjU2",
  "tracks" : {
    "href" : "https://api.spotify.com/v1/playlists/37i9dQZF1DXc5e2bJhV6pu/tracks?offset=0&limit=100",
    "items" : [ ** removed to shorten the log, but here were absolutly fine track objects **, {
      "added_at" : "2019-08-22T23:17:32Z",
      "added_by" : {
        "external_urls" : {
          "spotify" : "https://open.spotify.com/user/"
        },
        "href" : "https://api.spotify.com/v1/users/",
        "id" : "",
        "type" : "user",
        "uri" : "spotify:user:"
      },
      "is_local" : false,
      "primary_color" : null,
      "track" : null,
      "video_thumbnail" : {
        "url" : null
      }
    }, ** removed to shorten the log, but here were absolutly fine track objects ** ],
    "limit" : 100,
    "next" : null,
    "offset" : 0,
    "previous" : null,
    "total" : 60
  },
  "type" : "playlist",
  "uri" : "spotify:playlist:37i9dQZF1DXc5e2bJhV6pu"
}
	at com.adamratzman.spotify.endpoints.public.PlaylistAPI$getPlaylist$1$1.invoke(PlaylistAPI.kt:140)
	at com.adamratzman.spotify.endpoints.public.PlaylistAPI$getPlaylist$1$1.invoke(PlaylistAPI.kt:29)
	at com.adamratzman.spotify.utils.MiscUtilsKt.catch(MiscUtils.kt:11)
	at com.adamratzman.spotify.endpoints.public.PlaylistAPI$getPlaylist$1.get(PlaylistAPI.kt:76)
	at com.adamratzman.spotify.endpoints.public.PlaylistAPI$getPlaylist$1.get(PlaylistAPI.kt:29)
	at com.adamratzman.spotify.SpotifyRestAction.complete(SpotifyRestAction.kt:34)
	at com.adamratzman.spotify.SpotifyRestAction$queue$3.run(SpotifyRestAction.kt:51)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run(FutureTask.java:266)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Caused by: com.squareup.moshi.JsonDataException: Non-null value 'track' was null at $.tracks.items[50].track
	at com.squareup.moshi.kotlin.reflect.KotlinJsonAdapter.fromJson(KotlinJsonAdapter.kt:86)
	at com.squareup.moshi.JsonAdapter$2.fromJson(JsonAdapter.java:137)
	at com.squareup.moshi.CollectionJsonAdapter.fromJson(CollectionJsonAdapter.java:76)
	at com.squareup.moshi.CollectionJsonAdapter$2.fromJson(CollectionJsonAdapter.java:53)
	at com.squareup.moshi.JsonAdapter$2.fromJson(JsonAdapter.java:137)
	at com.squareup.moshi.kotlin.reflect.KotlinJsonAdapter.fromJson(KotlinJsonAdapter.kt:83)
	at com.squareup.moshi.JsonAdapter$2.fromJson(JsonAdapter.java:137)
	at com.squareup.moshi.kotlin.reflect.KotlinJsonAdapter.fromJson(KotlinJsonAdapter.kt:83)
	at com.squareup.moshi.JsonAdapter$2.fromJson(JsonAdapter.java:137)
	at com.squareup.moshi.JsonAdapter.fromJson(JsonAdapter.java:41)
	at com.adamratzman.spotify.endpoints.public.PlaylistAPI$getPlaylist$1$1.invoke(PlaylistAPI.kt:132)
	... 13 more

To Reproduce
Well, hard to say, because there is no bug in this library, but one in the Spotify Web API. You can use the PlaylistAPI.getPlaylistTracks endpoint of playlist spotify:playlist:37i9dQZF1DXc5e2bJhV6pu currently. But this may change in future...

Expected behavior
Not shure, but I'm okay with the current behaviour. I'm just opening this issue, to make other users of the library aware of it. For me it's a wontfix issue... Should be fixed by Spotify.

See spotify/web-api#958 for current information.

Dependabot couldn't find a build.gradle for this project

Dependabot couldn't find a build.gradle for this project.

Dependabot requires a build.gradle to evaluate your project's current Java dependencies. It had expected to find one at the path: /build.gradle.

If this isn't a Java project, or if it is a library, you may wish to disable updates for it from within Dependabot.

Cache isn't thread-safe

SpotifyCache is not thread-safe and thus concurrent modification raises exceptions. To fix, force synchronization or change request map type to ConcurrentHashMap

java.util.ConcurrentModificationException
	at java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:719)
	at java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:752)
	at java.util.LinkedHashMap$LinkedEntryIterator.next(LinkedHashMap.java:750)
	at java.util.Collection.removeIf(Collection.java:414)
	at com.adamratzman.spotify.http.SpotifyCache.checkCache(Endpoints.kt:173)
	at com.adamratzman.spotify.http.SpotifyCache.set$spotify_api_kotlin(Endpoints.kt:160)
	at com.adamratzman.spotify.http.SpotifyEndpoint.handleResponse(Endpoints.kt:96)
	at com.adamratzman.spotify.http.SpotifyEndpoint.execute(Endpoints.kt:70)
	at com.adamratzman.spotify.http.SpotifyEndpoint.execute$default(Endpoints.kt:49)
	at com.adamratzman.spotify.http.SpotifyEndpoint.get$spotify_api_kotlin(Endpoints.kt:25)
	at com.adamratzman.spotify.endpoints.public.SearchAPI$searchTrack$1.get(SearchAPI.kt:211)
	at com.adamratzman.spotify.endpoints.public.SearchAPI$searchTrack$1.get(SearchAPI.kt:28)
	at com.adamratzman.spotify.SpotifyRestAction.complete(SpotifyRestAction.kt:34)
	at com.adamratzman.website.endpoints.SpotifyRecommenderKt$autocompleteSpotifySearch$1$tracksDeferred$1.invokeSuspend(SpotifyRecommender.kt:170)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:241)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:270)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:79)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at com.adamratzman.website.endpoints.SpotifyRecommenderKt.autocompleteSpotifySearch(SpotifyRecommender.kt:157)
	at com.adamratzman.website.endpoints.SpotifyRecommenderKt.access$autocompleteSpotifySearch(SpotifyRecommender.kt:1)
	at com.adamratzman.website.endpoints.SpotifyRecommenderKt$spotify$1$3.handle(SpotifyRecommender.kt:85)
	at com.adamratzman.website.endpoints.SpotifyRecommenderKt$spotify$1$3.handle(SpotifyRecommender.kt)
	at spark.RouteImpl$1.handle(RouteImpl.java:72)
	at spark.http.matching.Routes.execute(Routes.java:61)
	at spark.http.matching.MatcherFilter.doFilter(MatcherFilter.java:130)
	at spark.embeddedserver.jetty.JettyHandler.doHandle(JettyHandler.java:50)
	at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1568)
	at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
	at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:132)
	at org.eclipse.jetty.server.Server.handle(Server.java:530)
	at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:347)
	at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:256)
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:279)
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:102)
	at org.eclipse.jetty.io.ChannelEndPoint$2.run(ChannelEndPoint.java:124)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.doProduce(EatWhatYouKill.java:247)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.produce(EatWhatYouKill.java:140)
	at org.eclipse.jetty.util.thread.strategy.EatWhatYouKill.run(EatWhatYouKill.java:131)
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:382)
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:708)
	at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:626)
	at java.lang.Thread.run(Thread.java:748)

`refresh_token` is overwritten on refresh

I'm getting:

com.adamratzman.spotify.utils.BadRequestException: Received Status Code 401. Error cause: Invalid access token
        at com.adamratzman.spotify.utils.SpotifyEndpoint.handleResponse(Endpoints.kt:98)
        at com.adamratzman.spotify.utils.SpotifyEndpoint.execute(Endpoints.kt:59)
       ...

Even though I got:

Spotify Logger Info: Successfully refreshed the Spotify token                                                                                                                         

I'll take a look at it though it's hard to debug because of the time for refresh.

Adding limit and offset to albums

Currently the ClientLibraryAPI.getSavedAlbums() does not support limit and offset parameters even though the Spotify API clearly supports it. It is an easy fix but for some reason your repository does not want to accept my pushes.

    fun getSavedAlbums(limit: Int? = null, offset: Int? = null, market: Market? = null): SpotifyRestAction<PagingObject<SavedAlbum>> {
        return toAction(Supplier {
            get(EndpointBuilder("/me/albums").with("limit", limit).with("offset", offset).with("market", market)
                    .build()).toPagingObject<SavedAlbum>(endpoint = this)
        })
    }

ClassCastException on simple search

Hi,
I tried a simple track search according to the documentation and example files, but I'm getting a ClassCast Exception when calling the search method.
What is wrong with my code?
I profiled the network an the REST Calls / Response seems to be OK

val result = api.search.searchTrack("quand je serai grand", market = Market.FR).complete();
println(result.items)

throws this:

java.lang.ClassCastException: org.json.JSONArray cannot be cast to java.lang.Iterable
     at com.adamratzman.spotify.endpoints.pub.search.SearchAPI$searchTrack$1.get(SearchAPI.kt:96)
     at com.adamratzman.spotify.endpoints.pub.search.SearchAPI$searchTrack$1.get(SearchAPI.kt:10)
     at com.adamratzman.spotify.utils.SpotifyRestAction.complete(SpotifyRestAction.kt:11)

Non-GET methods

All non-GET methods still need to be implemented. Use the provided PagingObject and CursorBasedPagingObject objects as a shortcut. SpotifyAPI class contains methods to transform Strings into their respective objects

update endpoints

Search endpoint includes a new optional field. Should go back through each endpoint and see if there are any updates

Dependabot couldn't find a build.gradle for this project

Dependabot couldn't find a build.gradle for this project.

Dependabot requires a build.gradle to evaluate your project's current Java dependencies. It had expected to find one at the path: /build.gradle.

If this isn't a Java project, or if it is a library, you may wish to disable updates for it from within Dependabot.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.