Photo by Fábio Alves on Unsplash

Android and Spotify Playlist APIs

I wanted to get my Spotify playlists and decided to use the Spotify APIs in an Android app to do just that. Fortunately, I didn’t need to do anything from scratch. There are three parts to getting everything to work.

  1. Create and Android app.
  2. Create app companion data on the Spotify for Developers website.
  3. Create a web application for Spotify account authentication.

Go to:

Clone the repo and build the project. The one we are interested in is located in auth-sample. You will need to fill in three fields: CLIENT_ID in MainActivity, and the two string values in res/values/spotify-strings.xml. Keep the project open but open a browser so we can get the CLIENT_ID.

Go to:

Register or sign in. Then create an app. You are actually going to input and generate app metadata. The package name you supply is the package that contains MainActivity from the Android app. The SHA-1 Fingerprint is retrieved from the android keystore located on your development machine. From a terminal window, try the command:

$ keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android | grep SHA1

If you changed the store or key passwords from their default values android, have the command above reflect those changes. You will see a long string of hexadecimal pairs separated by colons. That’s the SHA-1 Fingerprint you will provide. The redirect URIs are:

http://10.0.2.2:8888
http://10.0.2.2:8888/callback

Enter the application name, info, redirect URIs, Android package name and SHA-1 Fingerprint. Save. You will get a Client ID and Client Secret. Remember these two values.

We are done with the Spotify of Developers dashboard.

Go back to the Android project. Supply the CLIENT_ID value with that generated in the Spotify dashboard. Supply the string scheme value as http and the string host with 10.0.2.2:8888. Because the host string contains a colon (:) we need to change a line of code. At the very bottom of MainActivity, in the method redirectUri(), change the chain method authority() to encodedAuthority().

See if it compiles. I hope it does. Don’t run it yet.

Go to:

Open a terminal window. Clone the repo and run npm install, as the instructions say. If you don’t know about npm, look it up and install that first. Then come back here. Open authorization_code/app.js. Edit three lines:

var client_id = ‘CLIENT_ID’; // Your client id
var client_secret = ‘CLIENT_SECRET’; // Your secret
var redirect_uri = ‘REDIRECT_URI’; // Your redirect uri

The client id and secret you enter are from the Spotify dashboard. Enter localhost:8888 for the redirect uri. Then, as the instructions say:

$ cd authorization_code
$ node app.js

You should see a single line output:

Listening on 8888

Keep it running. You didn’t need to know anything about Node!

Run your Android app. I hope it runs. If it does, click on Request Token. Accept the terms. A long code should appear. Awesome! You have authenticated with Spotify.

Now onto the playlist code!

Hmm. The repository project code is in Java. I converted it to Kotlin. So my code snippets from here on are in Kotlin, but are based on the repository we both cloned.

Add:

onRequestTokenClicked(null)

as the last line in the method onCreate(), and add:

onGetPlaylistIDsClicked(null)

as the last line in the method onActivityResult(). This way, when the app starts, a new token is generated and received, and we start the process of getting our playlists. The buttons really don’t do anything for us anymore, but if you want to remove them, I leave that up to you. I just want my playlists so with the changes above we don’t need to press any buttons or remove any existing code.

Let’s build the new code and put in things that don’t exist. The endpoint prefix that will be for all calls to Spotify APIs, the base endpoint, is:

private val BASE_ENDPOINT = "https://api.spotify.com/v1"

Also, the entry method is:

private fun getPlaylistIDsAndNames(request: Request) {
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
setResponse("Failed to fetch data: $e")
}
@Throws(IOException::class)
override fun onResponse(call: Call, response: Response) {
addPlaylistIDsAndNames(JSONObject(
response.body()?.string() ?: ""))
}
})
}

This method will create a network request, and make the call asynchronously. The results will come to onResponse() when ready. We are using OkHttp. No need for Retrofit. The project already has the client, but I changed it to be lazily instantiated:

private val client by lazy {
OkHttpClient
.Builder()
.dispatcher(Dispatcher(Executors.newFixedThreadPool(5)))
.build()
}

The default number of available threads is two so I decided to create the client with five threads. If the default is fine with you, the above can be simplified to:

private val client by lazy { OkHttpClient() }

We get the response from our request, which contains our playlists, and move onto getting the tracks. First we will save the playlist info, which for us is simply the name and the id for each playlist. The name is what identifies the playlist for us. The id is what identifies the playlist in API requests and responses.

private val playlistIDs = arrayListOf<String>()
private val playlistNames = arrayListOf<String>()
private var total: AtomicInteger = AtomicInteger(0)
private val lock = Object()
private fun addPlaylistIDsAndNames(obj: JSONObject) {
total = AtomicInteger(obj["total"].toString().toInt())
val array = (obj["items"] as JSONArray)
synchronized(lock) {
for (i in 0 until array.length()) {
playlistIDs.add((array[i] as JSONObject)["id"]
.toString())
playlistNames.add((array[i] as JSONObject)["name"]
.toString())
}
}
val next = obj["next"].toString()
if (next != "null") {
getPlaylistIDsAndNames(getRequest(next))
} else {
getAllTracks()
}
}

If you look at any of the raw JSON responses, there is a lot of information. Including arrays in arrays. You just need to go through the response JSON object and figure out what you need and then get it. Yes, you can use a JSON converter, but the repository code went only as far as using OkHttp and it is simple enough to manually traverse the JSON objects and get what you need. And you don’t have extra libraries or classes to include. This project has one class: MainActivity.

Note: The article will now talk about synchronized code. I wrote another article after this one that changes synchronized() to ReentrantLock.withLock(). The article can be found here:

There’s concurrency going on. What to use? Coroutines? Executors? ReentrantLock? synchronized()? There’s lots of choices. I’m going to use synchronized() and AtomicInteger. (Note: Kotlin has deprecated synchronized()) We need to make sure that total and the lists and map do not have concurrency issues. Using synchronizedList() or synchronizedMap() will not work since they do not protect the iterator (look it up!), and we iterate over the lists and map. So I wrap the list and map operations in synchronized() , and use AtomicInteger for total. Easy-peasy. I could sneak total inside synchronized() and make it an Integer, but I left it outside.

Back to addPlaylistIDsAndNames(). total is the total number of playlists you have. That may not be the number of playlists actually returned in the response. The maximum number in any given response is 50. If you have more than 50 you will need to make multiple requests. If that’s the case, Spotify makes it really easy to do all that. Look for next. If it contains "null" then you’re done with getting your playlist ids and names. If not, next contains the next request. All you need to do is make another call to getPlaylistIDsAndNames() with that request. The foreach loop is to simply go through all the playlists info in items and save both the id and name for each playlist.

When we are done getting all the playlist ids and names we get the tracks for each and all playlists:

private val playlistTracks = 
mutableMapOf<String, ArrayList<String>>()
private fun getAllTracks() {
synchronized(lock) {
for ((index, id) in playlistIDs.withIndex()) {
playlistTracks[playlistIDs[index]] = arrayListOf()
getPlaylistTracks(getRequest(
"${BASE_ENDPOINT}/playlists/$id/tracks"))
}
}
}
private fun getPlaylistTracks(request: Request) {
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
setResponse("Failed to fetch data: $e")
}
@Throws(IOException::class)
override fun onResponse(call: Call, response: Response) {
addPlaylistTracks(JSONObject(
response.body()?.string() ?: ""))
}
})
}

For each playlist id getAllTracks() will make a request to get all of its tracks by calling getPlaylistTracks(). We need a place to store the results so we first create a map entry with the playlist id for the key and a list of track names for the value. We may also need to make multiple requests, so the map’s key will allow us to find and store multiple request responses for an individual playlist.

Since all playlist track requests in getPlaylistTracks() are concurrent (up to the number of threads in the OkHttp client), we need to make sure that concurrent modification on the map is allowed, so we use ConcurrentHashMap.

Once a request response is received we call addPlaylistTracks(), which will save the playlist track info received:

private fun addPlaylistTracks(obj: JSONObject) {
val array = (obj["items"] as JSONArray)
synchronized(lock) {
val trackID = getTrackIDFromHref(obj["href"].toString())
val list: ArrayList<String> =
playlistTracks[trackID] ?: ArrayList()
for (i in 0 until array.length()) {
list.add(((array[i] as JSONObject)["track"] as
JSONObject)["name"].toString())
}
playlistTracks[trackID] = list
}
val next = obj["next"].toString()
if (next != "null") {
getPlaylistTracks(getRequest(next))
} else {
if (total.decrementAndGet() == 0) addNames()
}
}
private fun getTrackIDFromHref(href: String): String {
for (key in playlistIDs) if (href.contains(key)) return key
return ""
}
private fun addNames() {
for (index in playlistIDs.indices) {
val list = playlistTracks[playlistIDs[index]]
?: arrayListOf()
playlistTracks.remove(playlistIDs[index])
playlistTracks["${playlistNames[index]}
[${playlistIDs[index]}]"] = list
}
printit()
}

Since addPlaylistTracks() will process a response without knowing which playlist tracks info it has, we need to figure that out by looking at href in the response, which contains the playlist id. That’s the work of getTrackIDFromHref(). Then we get the current list of the playlists’s tracks, look at the array items, get name from each array entry, add those names to the track list, then place the list back into the map. If we need to make another network request because the response doesn’t contain all the tracks, we look at next and make another call if the vale is not "null".

addPlaylistTracks() is called for every playlist and maybe multiple times per playlist, so it is a very active method. It gets a response, extracts the track names, and adds the tracks to the proper playlist through the map.

Remember when we saved total in the method addPlaylistIDsAndNames()? It is the total number of playlists you have. As we finish getting all the tracks for a particular playlist we decrement that value and when it gets to zero, we are done with all the requests!

Finally, the printing of the playlists and tracks:

private fun printit() {
for ((name, tracks) in playlistTracks) {
print name
for (track in tracks) {
print track
}
}

I leave it up to you to print out each playlist name and track as you see fit.

Thank you for reading. Get out there and code!

--

--

--

Developer at JPMorgan Chase. Everything Android, Math, Quantum, Physics, Cosmology, Maps and yes, Mainframe.

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

API Developer Portal — The What, The Why and The How

Configure a Pod to Use a Volume for Storage — Ansible module k8s

8 Tips For Using Python Logging

My Pentest Log -15- (HTML Injection in Wordpress)

Data Engineering Best Practices

Leetcode Q304. Range Sum Query 2D — Immutable (Q260)

Creator of the Week: 100+ Habits on a journey to improve your life

Dependency Injection in TypeScript

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Chris Ribetti

Chris Ribetti

Developer at JPMorgan Chase. Everything Android, Math, Quantum, Physics, Cosmology, Maps and yes, Mainframe.

More from Medium

Create a Simple Counter App : Kotlin Basics

FASTLANE: FROM ZERO TO AUTO

How to Integrate Ortho in Your Android App

Espresso UI Testing for Intents in Android