Chandra Sivaraman
Software Engineering Notes

Spotify Artist Follow05 May 2022

Spotify Artist Follow Photo by Jehyun Sung on Unsplash

I recently helped with integrating Spotify APIs into a Wordpress website for a collective of musicians. The ask was for a public user to be able to follow a group of artists on Spotify through a single button click. It seemed to be simple enough, but as all engineers know, reality has a surprising amount of detail. I wasn’t very familiar with PHP and relied heavily on Google. I did this on a Mac running Monterey, but most of the steps (except for website setup) are universally applicable.

Create Spotify App

To begin with, we need a free or premium Spotify user account. Then, we need to login to the Spotify Developer website, and create an app. Give it a name and a description.

Then, hit Edit Settings on the app:

Enter a callback URL (enter http://localhost/follow/callback.php for now - this is the URL for my local test website) and hit Add, followed by Save at the bottom (this is the link to our website that will be redirected to after Spotify authorization call):

Authorization Flow

I needed a way for a user to login to Spotify, and authorize my app to follow artists on their behalf. I found some sample code from this tutorial that did just that.

Based on the Authorization Guide, the Authorization Code Flow seemed like what I needed.

“In scenarios where storing the client secret is not safe (e.g. desktop, mobile apps or JavaScript web apps running in the browser), you can use the authorization code with PKCE, as it provides protection against attacks where the authorization code may be intercepted.”

Follow API

Next, I needed a bulk API to follow artists. I found this one, to which I needed to pass the access token from the authorization call.

curl PUT https://api.spotify.com/v1/me/following?ids=artist1,artist2,...,artistn
-H "Authorization: Bearer BQD.....jwq6c1PBd"

Local test website setup

Since it was a Wordpress site, the backend had to be PHP. I created three PHP files - login.php for the authorization, callback.php to handle the callback from Spotify’s authorize endpoint, and follow.php for the artist follow call.

Next, I setup a local PHP web server. PHP comes bundled with Mac OS pre-Monterey. Post-Monterey, it has to be installed using Homebrew. From a terminal window, type (this takes a while to finish):

brew install PHP

There was a lengthy detour to self sign PHP, since Monterey doesn’t accept unsigned packages. This involved setting up a local certificate authority and a code signing certificate.

Then, in the apache root folder (usually /Library/WebServer/Documents), I created a follow folder and dropped the index.html, login.php and follow.php files into it.

Home page

index.html is my one page test website. It uses handlebars to populate markup with results from the API calls, and Jquery to call the Spotify APIs.

Login button

The Login with Spotify button redirects to login.php:

document.getElementById('login-button').addEventListener('click', function() {
	window.location = 'http://localhost/follow/login.php';   
}, false);

login.php redirects to Spotify’s authorize endpoint (the response_type must be set to code for the authorization code, and the redirect URI passed has to match the redirect URI in the Spotify app settings) :

$client_id = "<client id from the Spotify app in developer dashboard>";  
$state = generateRandomString(16);

$scope = "user-follow-modify";

$url = "https://accounts.spotify.com/authorize";
$url .= "?response_type=code";
$url .= "&client_id=" . urlencode($client_id);
$url .= "&scope=" . urlencode($scope);
$url .= "&redirect_uri=" . urlencode($redirect_uri);
$url .= "&state=" . urlencode($state);

header("Location: " . $url);

Spotify authorize endpoint redirects back to http://localhost/follow/callback.php passing it the authorization code. callback.php exchanges this code for an access token by POSTing to the https://accounts.spotify.com/api/token endpoint. It then calls the /v1/me endpoint with this token to get the user’s display name and user id. It cookies this profile info, encrypts and cookies the access token and redirects back to the index.html page:

// get token & state from url 
$error = isset($_GET["error"]) ? $_GET["error"] : NULL;
$code = $_GET["code"];
$state = $_GET["state"];
$url = "/follow";   
$client_id = config()["client_id"];
$client_secret = config()["client_secret"];
$redirect_uri = config()["redirect_url"];

$url = "https://accounts.spotify.com/api/token";

$fields = [
    "code"          => urlencode($code),
    "redirect_uri"  => $redirect_uri,
    "grant_type"    => "authorization_code",
    "state" => $state
];
    
//url-ify the data for the POST
$fields_string = http_build_query($fields);

$result = http_request("POST", $url, 
    array(
        'authorization: Basic ' . base64_encode($client_id . ":" . $client_secret)
    ),
    $fields_string);

if ($result !== FALSE && ($result === "" || !array_key_exists("error", json_decode($result,true)))) {    
    $result_arr = json_decode($result, true);
    $access_token = $result_arr["access_token"];
    $enc_access_token = encrypt($access_token); 

    setcookie("t", $enc_access_token, time()+1200); 
    setcookie("state", $state, time()+1200); 
    header("Location: /follow");

    // get the client-id & name
    $url = 'https://api.spotify.com/v1/me';   
    $result = http_request("GET", $url, 
        array(
            'authorization: Bearer ' . $access_token
        ));

    if ($result !== FALSE && ($result === "" || !array_key_exists("error", json_decode($result,true)))) {    
        $result_arr = json_decode($result, true);
        $display_name = $result_arr["display_name"];
        $user_id = $result_arr["id"];
        setcookie("display_name", $display_name);
        setcookie("user_id", $user_id);
        header("Location: /follow");	

Profile information

On the index.html page, we hide the login button, and display the logout and Follow All button.

Follow button

The Follow All button redirects to follow.php passing in encrypted access_token and user_id:

document.getElementById('follow-button').addEventListener('click', function() {
	var url = `/follow/follow.php?access_token=${access_token}&id=${user_id}`;
	$.ajax({type: 'GET', url: url})
	.done(function(response) {
	  artistsFollowedPlaceholder.innerHTML = artistsFollowedTemplate(response);
	})
	.fail(function(msg) {
	  $('#errorMsg').text(msg.responseJSON.msg); 
	});
}, false);

follow.php calls the Spotify /follow API. The /follow API is limited to 50 artists per call. I got around that by calling /follow in batches of 50 at a time. Then, I call /artists?ids= to get the artist names and links, which are returned as the response to this call. An admin email is sent as well to track users who followed:

$access_token = decrypt(str_replace(" ", "+", $_GET["access_token"]));
$spotify_id = $_GET["id"];
$artist_ids = config()["artist_ids"];
$api_root_url = "https://api.spotify.com/v1";
$startIdx = 0;
$more = True;
$artist_ids_arr = str_getcsv($artist_ids);
$batch_size = 50;
$result_json = [];
$followed = FALSE;

while ($more) {
    $artist_ids_batch = join(",", array_slice($artist_ids_arr, $startIdx, $batch_size));
    $url = $api_root_url . "/me/following?type=artist&ids=" . $artist_ids_batch;
    $result = http_request("PUT", $url, array('authorization: Bearer ' . $access_token));
    if ($result !== FALSE && ($result === "" || !array_key_exists("error", json_decode($result,true)))) {
        $url = $api_root_url . "/artists?ids=" . $artist_ids_batch;

        $result = http_request("GET", $url, array('authorization: Bearer ' . $access_token));
        $result_json = array_merge($result_json, json_decode($result,true)["artists"]);

        $followed = True;
    }
    else {
        $followed = False;
        $result_json = json_decode("{\"msg\": \"" . "Follow artists failed with error: " . json_decode($result,true)["error"]["message"] . "\"}");
        break;
    }
    $startIdx = $startIdx + $batch_size;
    if (count($artist_ids_arr)-1 < $startIdx) {
        $more = False;
    } 
}

if ($followed) {
    mail(config()["admin_email"], $spotify_id . "followed all artists on Auricle collective", "");
}
else {
    mail(config()["admin_email"], $spotify_id . "Failed to follow all artists on Auricle collective", "");
}

header('Content-Type: application/json; charset=utf-8');
http_response_code($followed ? 200 : 500);
echo(json_encode($result_json));

Logout button

When I submitted a quota extension request for the app, Spotify asked me to provide a way for users to disconnect from Spotify. I added a logout button which clears cookies and redirects to logout.php:

document.getElementById('logout-button').addEventListener('click', function() {
	Cookies.remove("t", { path: '' });
	Cookies.remove("state", { path: '' });
	Cookies.remove("error", { path: '' });
	window.location = "/follow/logout.php";
}, false);

logout.php in turn redirects to Spotify’s logout URL:

<?php
    header("Location: https://www.spotify.com/logout/");
?>

This seems to work fine on Chrome. On Safari and Firefox, however, hitting the back button shows the user as logged in. It seems to be loading the page from cache. I tried putting in the usual HTTP no-cache headers, but Safari wouldn’t budge. After some thrashing, I found this snippet on Stackoverflow which did the trick. It reloads the page :

window.addEventListener("pageshow", function(evt){
        if(evt.persisted){
        setTimeout(function(){
            window.location.reload();
        },10);
    }
}, false);

App flow

The flow looks like this:

Source code for this article

Github

Final notes: