Revising the ESP32 Spotify Display with ChatGPT

Some of you may remember my device that shows what song is playing on Spotify. It looks like this, and it still won’t win any design prizes:

The famous Spotify Song Display
The famous Spotify Song Display

I posted that article on April 18, 2022, and this device has been running smooth ever since, except last Wednesday when it conked out. By plugging this back into a computer I can see what serial monitor has to say about the situation, and it revealed that my access codes had gone stale.

As described in the previous article, I used a library by Brian Clough to access the Spotify API, and it was very intuitive. However, this time around, I didn’t get a viable response from Spotify using the library. I tried all I could, but to no avail. I did hear from a Github user that the library had been updated, and should work after applying the update, but before I heard that heads-up, I had already decided to see what ChatGPT would deliver in this situation.

Setting the basics

Even though I knew how to operate the Spotify API, I asked this of ChatGPT:

How can I get a refresh token using Spotify API and ESP32?

And this was the result:

To obtain a refresh token using the Spotify API and an ESP32 microcontroller, you’ll need to follow these general steps:

  1. Create a Spotify Developer Account: Go to the Spotify Developer Dashboard (https://developer.spotify.com/dashboard/) and create a new application. This will give you the client ID and client secret needed for authentication.
  2. Set up OAuth2 Authorization Flow: Configure your Spotify application to use the Authorization Code Flow. This involves setting up redirect URIs and scopes.
  3. Implement OAuth2 Authorization Flow on ESP32: Your ESP32 device needs to handle the OAuth2 authorization flow. This typically involves:
    • Opening a web server on the ESP32.
    • Redirecting the user to the Spotify authorization endpoint where they can grant access to your application.
    • Handling the callback from Spotify and exchanging the authorization code for an access token and refresh token.
  4. Store the Refresh Token Securely: Once you obtain the refresh token, store it securely on the ESP32 device. Make sure to encrypt it if possible to enhance security.
  5. Use the Refresh Token to Get New Access Tokens: Periodically, use the refresh token to obtain a new access token. This ensures that your ESP32 device can continue to access the Spotify API without requiring the user to reauthorize.

This sounded promising, since it also gave me code to upload to my ESP32, access the webpage created by that code on the ESP32 (you need to go there with your mobile phone in the same WLAN as the ESP32), and then click on the link you see on that page:

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
const char* ssid = "YourWiFiSSID";
const char* password = "YourWiFiPassword";
WiFiServer server(80);
const char* spotify_client_id = "YourSpotifyClientID";
const char* spotify_client_secret = "YourSpotifyClientSecret";
const char* redirect_uri = "http://your-esp32-ip/callback";
String refresh_token;
void setup() {
  Serial.begin(115200);
  delay(10);
  // Connect to WiFi
  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected.");

  // Start the server
  server.begin();
  Serial.println("Server started");

  // Print the ESP32 IP address
  Serial.println(WiFi.localIP());
}
void loop() {
  // Check if a client has connected
  WiFiClient client = server.available();
  if (!client) {
    return;
  }
  // Wait until the client sends some data
  Serial.println("New client");
  while (!client.available()) {
    delay(1);
  }
  // Read the HTTP request
  String request = client.readStringUntil('\r');
  Serial.println(request);
  client.flush();
  // Check if the request contains the authorization code
  if (request.indexOf("GET /callback") != -1) {
    // Extract authorization code from request
    int codeIndex = request.indexOf("code=");
    int codeEndIndex = request.indexOf("HTTP");
    String authorization_code = request.substring(codeIndex + 5, codeEndIndex - 1);
    // Exchange authorization code for tokens
    exchangeAuthorizationCode(authorization_code);
    // Send response to the client
    client.println("HTTP/1.1 200 OK");
    client.println("Content-type:text/html");
    client.println();
    client.println("<html>");
    client.println("<head><title>ESP32 Spotify Auth</title></head>");
    client.println("<body>");
    client.println("<h1>Authorization Successful!</h1>");
    client.println("</body>");
    client.println("</html>");
    delay(1);
    client.stop();
  } else {
    // Redirect the user to Spotify authorization endpoint
    redirectToSpotify(client);
  }
}
void redirectToSpotify(WiFiClient client) {
  // Redirect to Spotify authorization URL
  client.println("HTTP/1.1 302 Found");
  client.println("Location: https://accounts.spotify.com/authorize"
                 "?client_id=" + String(spotify_client_id) +
                 "&response_type=code" +
                 "&redirect_uri=" + String(redirect_uri) +
                 "&scope=user-read-private%20user-read-email" +
                 "&state=123");
  client.println();
}
void exchangeAuthorizationCode(String code) {
  HTTPClient http;
  http.begin("https://accounts.spotify.com/api/token");
  http.addHeader("Content-Type", "application/x-www-form-urlencoded");
  String postData = "grant_type=authorization_code"
                    "&code=" + code +
                    "&redirect_uri=" + String(redirect_uri) +
                    "&client_id=" + String(spotify_client_id) +
                    "&client_secret=" + String(spotify_client_secret);
  int httpResponseCode = http.POST(postData);
  if (httpResponseCode > 0) {
    String payload = http.getString();
    Serial.println("Token Response: " + payload);
    // Extract and store refresh token
    refresh_token = getValue(payload, "refresh_token");
  } else {
    Serial.println("Error exchanging code for token");
  }
  http.end();
}
String getValue(String data, String key) {
  int startIndex = data.indexOf(key + "\":\"") + key.length() + 3;
  int endIndex = data.indexOf("\"", startIndex);
  return data.substring(startIndex, endIndex);
}

And hey presto, this will deliver the remaining piece of the puzzle, an access token. However, it will only live for an hour so I asked this next:

How can I get the token to live longer than an hour?

And again, service-oriented as always, ChatGPT delivered an edited code that will make sure the access code is automatically revived before it goes stale. At this point I had some issues with ESP32 memory management, and the device went into a boot loop, but when I posted the error message for ChatGPT to solve, it did so in an efficient manner. Soon I had the access token and refresh token, and I was able to ask:

Okay, I got a token. Please write me code for ESP32 to get the current song, artist, and album.

This resulted in a piece of code that enabled me to see all the data of the song that was playing in JSON format on the serial monitor, so I asked ChatGPT to pick just the song name, artist, and album from that torrent of data. It did so and for 40 minutes I had what I wanted. But then the connection died again, and I complained to ChatGPT:

For a while I could get the soongnames, but now I get 
16:07:18.414 -> Song: null 
16:07:18.414 -> Artist: null 
16:07:18.414 -> Album: null

The response was quick and complete:

  • If you’re receiving null values for the song name, artist, and album, it indicates that the API response isn’t returning the expected data. There could be a few reasons for this:
  • Access Token Expiry: The access token you’re using might have expired. Access tokens typically have a limited lifespan and need to be refreshed periodically using the refresh token. Make sure your access token is still valid.
  • Rate Limiting: Spotify’s API has rate limits, and if you exceed them, you may receive empty or null responses. Check if you’re making too many requests in a short period. If so, consider slowing down the rate of requests.
  • Scope: Ensure that the access token you’re using has the necessary scope (user-read-currently-playing) to access the currently playing track. If the scope is missing or insufficient, the API response may not include the track information.
  • API Changes: Spotify may have made changes to their API, affecting the response structure. Check the Spotify API documentation or changelog to see if there are any recent updates that could be causing the issue.
  • Error Handling: Implement proper error handling in your code to capture any errors or unexpected responses from the Spotify API. Log the full response to see if there are any error messages or status codes that could provide more information about the issue.

Then I asked for complete code that would refresh all it had to refresh, and now I had the perfect response – minus the display of course.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

const char* ssid = "YourWiFiSSID";
const char* password = "YourWiFiPassword";
// Spotify API credentials
const char* spotify_client_id = "YourSpotifyClientID";
const char* spotify_client_secret = "YourSpotifyClientSecret";
const char* redirect_uri = "http://localhost/callback";
String refresh_token = "YourRefreshToken"; // Store your refresh token here
String access_token;
unsigned long access_token_expires_at = 0;
void setup() {
Serial.begin(115200);
delay(10);
// Connect to WiFi
Serial.println();
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println();
Serial.println("WiFi connected.");
// Set initial access token expiration time to 0 to force refresh on first loop iteration
access_token_expires_at = 0;
}
void loop() {
// Refresh access token if needed
if (needsTokenRefresh()) {
refreshAccessToken();
}
// Get currently playing song information
getCurrentSongInfo();
// Delay before next request
delay(5000); // Adjust as needed
}
bool needsTokenRefresh() {
// Refresh token 5 minutes before expiration
return (access_token_expires_at < millis() + 300000); } void refreshAccessToken() { HTTPClient http; http.begin("https://accounts.spotify.com/api/token"); http.addHeader("Content-Type", "application/x-www-form-urlencoded"); String postData = "grant_type=refresh_token" "&refresh_token=" + refresh_token + "&client_id=" + spotify_client_id + "&client_secret=" + spotify_client_secret; int httpResponseCode = http.POST(postData); if (httpResponseCode == 200) { String payload = http.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, payload); access_token = doc["access_token"].as();
int expires_in = doc["expires_in"].as();
access_token_expires_at = millis() + (expires_in * 1000);
Serial.println("Access token refreshed.");
} else {
Serial.println("Failed to refresh access token.");
}
http.end();
}
void getCurrentSongInfo() {
HTTPClient http;
http.begin("https://api.spotify.com/v1/me/player/currently-playing");
http.addHeader("Authorization", "Bearer " + access_token);
int httpResponseCode = http.GET();
if (httpResponseCode == 200) {
String payload = http.getString();
DynamicJsonDocument doc(2048);
deserializeJson(doc, payload);
if (doc.containsKey("item")) {
String songName = doc["item"]["name"].as();
String artistName = doc["item"]["artists"][0]["name"].as();
String albumName = doc["item"]["album"]["name"].as();
Serial.println("Currently Playing:");
Serial.println("Song: " + songName);
Serial.println("Artist: " + artistName);
Serial.println("Album: " + albumName);
} else {
Serial.println("No song currently playing.");
}
} else {
Serial.println("Failed to get currently playing song.");
}
http.end();
}

After this, all I did was tell the system which libraries I use for my e-Paper display, and asked it to deliver the data on both serial monitor and the e-Paper display. This it did flawlessly. I then looked at my old code and found the correct font, size, and cursor position settings, and pasted them in.

This gave me a nearly perfect system, but the last thing I did manually was add a string variable called mySong, which I set to maintain the name of the song that was already shown on the screen. I use this as a flag to decide whether to update the screen or not. Without this variable, the screen will refresh with the attached flicker effect every ten seconds, as the ESP32 refreshes the song data. I set the delay to two seconds, and when comparing songName to mySong in the loop, it will only update the display if they differ.

So now I have the complete working solution back on the display. I omitted some discussion elements due to the repeating nature of the exchanges, but the gist of the discussion is included.

What to make of all this?

I’d say that when you know exactly what you want ChatGPT to deliver, it will do so. If you don’t know, or you are not sure of the responses you get from the ESP32 you are working on, this may cause confusion and wild goose chases for solutions.

But when you are comfortable working with Arduinos or ESP32s, ChatGPT is a damned fine tool for getting stuff done faster and more efficiently.

If someone wants to see the final code, I can post it here or on GitHub.

Loading

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.