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.
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):
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.”
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"
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.
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.
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 POST
ing 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");
On the index.html
page, we hide the login button, and display the logout and Follow All
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));
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);
The flow looks like this:
After publishing this code to your server, update the redirect URI in the Developer dashboard and in login.php
.
The HTTP calls in PHP should ideally be asynchronous, but I don’t know enough PHP to do that and this app isn’t expected to have very high volume, so synchronous is good enough.
The Spotify app starts out in Development mode. This means that any user other than yourself has to be explicitly granted access to the app through the Developer dashboard in order to use it. Once you have tested it, you can submit a quota extension request to make it publicly accessible for all users. The first time I did this, Spotify rejected it and asked me to provide a logout feature, add their logo, and provide links to artist content on Spotify. I had to resubmit and am still waiting to hear back.