#include <ArduinoJson.h>
#include <base64.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <WebServer.h>
#include <base64.h>
#include <HTTPClient.h>
// Load tabs attached to this sketch
#include "index.h"
// Include the TFT library https://github.com/Bodmer/TFT_eSPI
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
Adafruit_SSD1306 display(128, 64, &Wire, -1);
/*=========================
|User modifiable variables|
=========================*/
// WiFi credentials
#define WIFI_SSID "Wokwi-GUEST"
#define PASSWORD ""
// Spotify API credentials
#define CLIENT_ID "xxxx"
#define CLIENT_SECRET "xxx"
#define REDIRECT_URI "http://localhost/callback"
/*=========================
|Non - modifiable variables|
==========================*/
String getValue(HTTPClient &http, String key) {
bool found = false, look = false, seek = true;
int ind = 0;
String ret_str = "";
int len = http.getSize();
char char_buff[1];
WiFiClient * stream = http.getStreamPtr();
while (http.connected() && (len > 0 || len == -1)) {
size_t size = stream->available();
// Serial.print("Size: ");
// Serial.println(size);
if (size) {
int c = stream->readBytes(char_buff, ((size > sizeof(char_buff)) ? sizeof(char_buff) : size));
if (found) {
if (seek && char_buff[0] != ':') {
continue;
} else if(char_buff[0] != '\n'){
if(seek && char_buff[0] == ':'){
seek = false;
int c = stream->readBytes(char_buff, 1);
}else{
ret_str += char_buff[0];
}
}else{
break;
}
// Serial.print("get: ");
// Serial.println(get);
}
else if ((!look) && (char_buff[0] == key[0])) {
look = true;
ind = 1;
} else if (look && (char_buff[0] == key[ind])) {
ind ++;
if (ind == key.length()) found = true;
} else if (look && (char_buff[0] != key[ind])) {
ind = 0;
look = false;
}
}
}
// Serial.println(*(ret_str.end()));
// Serial.println(*(ret_str.end()-1));
// Serial.println(*(ret_str.end()-2));
if(*(ret_str.end()-1) == ','){
ret_str = ret_str.substring(0,ret_str.length()-1);
}
return ret_str;
}
//http response struct
struct httpResponse{
int responseCode;
String responseMessage;
};
struct songDetails{
int durationMs;
String album;
String artist;
String song;
String Id;
bool isLiked;
};
void printSplitString(String text, int maxLineSize, int xPos, int yPos) {
int len = text.length();
int startPos = 0;
int tempx=xPos;
for (int i = 0; i < len; i++) {
if (text[i] == ' ' || i == len - 1) {
// Found a space or reached the end of the string
String word = text.substring(startPos, i + 1);
int16_t x, y;
uint16_t w, h;
display.getTextBounds(word.c_str(), 0, 0, &x, &y, &w, &h);
if (xPos + w > maxLineSize) {
// Move to a new line if the word exceeds the max line size
yPos += h; // Use height instead of textSize for more accurate positioning
display.setCursor(0, yPos);
xPos = tempx; // Reset xPos for new line
}
display.setCursor(xPos, yPos);
display.print(word);
startPos = i + 1;
if (i == len - 1) {
// Reached the end of the string, move to a new line
yPos += h; // Use height instead of textSize for more accurate positioning
display.setCursor(0, yPos);
} else {
// Move xPos to the end of the printed word for the next word
xPos += w;
}
}
}
display.display();
}
//Create spotify connection class
class SpotConn {
public:
SpotConn(){
client = std::unique_ptr<WiFiClientSecure>(new WiFiClientSecure());
client->setInsecure();
}
bool getUserCode(String serverCode) {
https.begin(*client,"https://accounts.spotify.com/api/token");
String auth = "Basic " + base64::encode(String(CLIENT_ID) + ":" + String(CLIENT_SECRET));
https.addHeader("Authorization",auth);
https.addHeader("Content-Type","application/x-www-form-urlencoded");
String requestBody = "grant_type=authorization_code&code="+serverCode+"&redirect_uri="+String(REDIRECT_URI);
// Send the POST request to the Spotify API
int httpResponseCode = https.POST(requestBody);
// Check if the request was successful
if (httpResponseCode == HTTP_CODE_OK) {
String response = https.getString();
DynamicJsonDocument doc(1024);
deserializeJson(doc, response);
accessToken = String((const char*)doc["access_token"]);
refreshToken = String((const char*)doc["refresh_token"]);
tokenExpireTime = doc["expires_in"];
tokenStartTime = millis();
accessTokenSet = true;
Serial.println(accessToken);
Serial.println(refreshToken);
}else{
Serial.println(https.getString());
}
// Disconnect from the Spotify API
https.end();
return accessTokenSet;
}
bool refreshAuth(){
https.begin(*client,"https://accounts.spotify.com/api/token");
String auth = "Basic " + base64::encode(String(CLIENT_ID) + ":" + String(CLIENT_SECRET));
https.addHeader("Authorization",auth);
https.addHeader("Content-Type","application/x-www-form-urlencoded");
String requestBody = "grant_type=refresh_token&refresh_token="+String(refreshToken);
// Send the POST request to the Spotify API
int httpResponseCode = https.POST(requestBody);
accessTokenSet = false;
// Check if the request was successful
if (httpResponseCode == HTTP_CODE_OK) {
String response = https.getString();
DynamicJsonDocument doc(1024);
deserializeJson(doc, response);
accessToken = String((const char*)doc["access_token"]);
// refreshToken = doc["refresh_token"];
tokenExpireTime = doc["expires_in"];
tokenStartTime = millis();
accessTokenSet = true;
Serial.println(accessToken);
Serial.println(refreshToken);
}else{
Serial.println(https.getString());
}
// Disconnect from the Spotify API
https.end();
return accessTokenSet;
}
bool getTrackInfo(){
String url = "https://api.spotify.com/v1/me/player/currently-playing";
https.useHTTP10(true);
https.begin(*client,url);
String auth = "Bearer " + String(accessToken);
https.addHeader("Authorization",auth);
int httpResponseCode = https.GET();
bool success = false;
String songId = "";
bool refresh = false;
// Check if the request was successful
if (httpResponseCode == 200) {
String currentSongProgress = getValue(https,"progress_ms");
currentSongPositionMs = currentSongProgress.toFloat();
String imageLink = "";
String albumName = getValue(https,"name");
String artistName = getValue(https,"name");
String songDuration = getValue(https,"duration_ms");
currentSong.durationMs = songDuration.toInt();
String songName = getValue(https,"name");
songId = getValue(https,"uri");
String isPlay = getValue(https, "is_playing");
isPlaying = isPlay == "true";
Serial.println(isPlay);
// Serial.println(songId);
songId = songId.substring(15,songId.length()-1);
Serial.println(songId);
Serial.println(currentSong.song);
https.end();
currentSong.album = albumName.substring(1,albumName.length()-1);
currentSong.artist = artistName.substring(1,artistName.length()-1);
currentSong.song = songName.substring(1,songName.length()-1);
currentSong.Id = songId;
currentSong.isLiked = findLikedStatus(songId);
success = true;
} else {
Serial.print("Error getting track info: ");
Serial.println(httpResponseCode);
// String response = https.getString();
// Serial.println(response);
https.end();
}
// Disconnect from the Spotify API
if(success){
//drawScreen(refresh);
lastSongPositionMs = currentSongPositionMs;
}
return success;
}
bool findLikedStatus(String songId){
String url = "https://api.spotify.com/v1/me/tracks/contains?ids="+songId;
https.begin(*client,url);
String auth = "Bearer " + String(accessToken);
https.addHeader("Authorization",auth);
https.addHeader("Content-Type","application/json");
int httpResponseCode = https.GET();
bool success = false;
// Check if the request was successful
if (httpResponseCode == 200) {
String response = https.getString();
https.end();
return(response == "[ true ]");
} else {
Serial.print("Error toggling liked songs: ");
Serial.println(httpResponseCode);
String response = https.getString();
Serial.println(response);
https.end();
}
// Disconnect from the Spotify API
return success;
}
bool toggleLiked(String songId){
String url = "https://api.spotify.com/v1/me/tracks/contains?ids="+songId;
https.begin(*client,url);
String auth = "Bearer " + String(accessToken);
https.addHeader("Authorization",auth);
https.addHeader("Content-Type","application/json");
int httpResponseCode = https.GET();
bool success = false;
// Check if the request was successful
if (httpResponseCode == 200) {
String response = https.getString();
https.end();
if(response == "[ true ]"){
currentSong.isLiked = false;
dislikeSong(songId);
}else{
currentSong.isLiked = true;
likeSong(songId);
}
//drawScreen(false,true);
Serial.println(response);
success = true;
} else {
Serial.print("Error toggling liked songs: ");
Serial.println(httpResponseCode);
String response = https.getString();
Serial.println(response);
https.end();
}
// Disconnect from the Spotify API
return success;
}
bool drawScreen(){
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 10);
// bool accessTokenSet = false;
// long tokenStartTime;
// int tokenExpireTime;
// songDetails currentSong;
// float currentSongPositionMs;
// float lastSongPositionMs;
// int currVol;
// struct songDetails{
// int durationMs;
// String album;
// String artist;
// String song;
// String Id;
// bool isLiked;};
printSplitString(currentSong.song, 128, 2, 5);
display.println(currentSong.artist);
display.println(currentSong.isLiked);
display.println(currentSongPositionMs);
display.println(currVol);
display.display();
}
bool togglePlay(){
String url = "https://api.spotify.com/v1/me/player/" + String(isPlaying ? "pause" : "play");
isPlaying = !isPlaying;
https.begin(*client,url);
String auth = "Bearer " + String(accessToken);
https.addHeader("Authorization",auth);
int httpResponseCode = https.PUT("");
bool success = false;
// Check if the request was successful
if (httpResponseCode == 204) {
// String response = https.getString();
Serial.println((isPlaying ? "Playing" : "Pausing"));
success = true;
} else {
Serial.print("Error pausing or playing: ");
Serial.println(httpResponseCode);
String response = https.getString();
Serial.println(response);
}
// Disconnect from the Spotify API
https.end();
getTrackInfo();
return success;
}
bool adjustVolume(int vol){
String url = "https://api.spotify.com/v1/me/player/volume?volume_percent=" + String(vol);
https.begin(*client,url);
String auth = "Bearer " + String(accessToken);
https.addHeader("Authorization",auth);
int httpResponseCode = https.PUT("");
bool success = false;
// Check if the request was successful
if (httpResponseCode == 204) {
// String response = https.getString();
currVol = vol;
success = true;
}else if(httpResponseCode == 403){
currVol = vol;
success = false;
Serial.print("Error setting volume: ");
Serial.println(httpResponseCode);
String response = https.getString();
Serial.println(response);
} else {
Serial.print("Error setting volume: ");
Serial.println(httpResponseCode);
String response = https.getString();
Serial.println(response);
}
// Disconnect from the Spotify API
https.end();
return success;
}
bool skipForward(){
String url = "https://api.spotify.com/v1/me/player/next";
https.begin(*client,url);
String auth = "Bearer " + String(accessToken);
https.addHeader("Authorization",auth);
int httpResponseCode = https.POST("");
bool success = false;
// Check if the request was successful
if (httpResponseCode == 204) {
// String response = https.getString();
Serial.println("skipping forward");
success = true;
} else {
Serial.print("Error skipping forward: ");
Serial.println(httpResponseCode);
String response = https.getString();
Serial.println(response);
}
// Disconnect from the Spotify API
https.end();
getTrackInfo();
return success;
}
bool skipBack(){
String url = "https://api.spotify.com/v1/me/player/previous";
https.begin(*client,url);
String auth = "Bearer " + String(accessToken);
https.addHeader("Authorization",auth);
int httpResponseCode = https.POST("");
bool success = false;
// Check if the request was successful
if (httpResponseCode == 204) {
// String response = https.getString();
Serial.println("skipping backward");
success = true;
} else {
Serial.print("Error skipping backward: ");
Serial.println(httpResponseCode);
String response = https.getString();
Serial.println(response);
}
// Disconnect from the Spotify API
https.end();
getTrackInfo();
return success;
}
bool likeSong(String songId){
String url = "https://api.spotify.com/v1/me/tracks?ids="+songId;
https.begin(*client,url);
String auth = "Bearer " + String(accessToken);
https.addHeader("Authorization",auth);
https.addHeader("Content-Type","application/json");
char requestBody[] = "{\"ids\":[\"string\"]}";
int httpResponseCode = https.PUT(requestBody);
bool success = false;
// Check if the request was successful
if (httpResponseCode == 200) {
// String response = https.getString();
Serial.println("added track to liked songs");
success = true;
} else {
Serial.print("Error adding to liked songs: ");
Serial.println(httpResponseCode);
String response = https.getString();
Serial.println(response);
}
// Disconnect from the Spotify API
https.end();
return success;
}
bool dislikeSong(String songId){
String url = "https://api.spotify.com/v1/me/tracks?ids="+songId;
https.begin(*client,url);
String auth = "Bearer " + String(accessToken);
https.addHeader("Authorization",auth);
// https.addHeader("Content-Type","application/json");
// char requestBody[] = "{\"ids\":[\"string\"]}";
//int httpResponseCode = https.DELETE();
int httpResponseCode = 200;
bool success = false;
// Check if the request was successful
if (httpResponseCode == 200) {
// String response = https.getString();
Serial.println("removed liked songs");
success = true;
} else {
Serial.print("Error removing from liked songs: ");
Serial.println(httpResponseCode);
String response = https.getString();
Serial.println(response);
}
// Disconnect from the Spotify API
https.end();
return success;
}
bool accessTokenSet = false;
long tokenStartTime;
int tokenExpireTime;
songDetails currentSong;
float currentSongPositionMs;
float lastSongPositionMs;
int currVol;
private:
std::unique_ptr<WiFiClientSecure> client;
HTTPClient https;
bool isPlaying = false;
String accessToken;
String refreshToken;
};
//Vars for keys, play state, last song, etc.
bool buttonStates[] = {1,1,1,1};
int debounceDelay = 50;
int temp = 0;
unsigned long debounceTimes[] = {0,0,0,0};
int buttonPins[] = {2,0,15,1};
//Object instances
WebServer server(80); //Server on port 80
SpotConn spotifyConnection;
//Web server callbacks
void handleRoot() {
Serial.println("handling root");
char page[500];
sprintf(page,mainPage,CLIENT_ID,REDIRECT_URI);
server.send(200, "text/html", String(page)+"\r\n"); //Send web page
}
void handleCallbackPage() {
if(!spotifyConnection.accessTokenSet){
if (server.arg("code") == ""){ //Parameter not found
char page[500];
sprintf(page,errorPage,CLIENT_ID,REDIRECT_URI);
server.send(200, "text/html", String(page)); //Send web page
}else{ //Parameter found
if(spotifyConnection.getUserCode(server.arg("code"))){
server.send(200,"text/html","Spotify setup complete Auth refresh in :"+String(spotifyConnection.tokenExpireTime));
}else{
char page[500];
sprintf(page,errorPage,CLIENT_ID,REDIRECT_URI);
server.send(200, "text/html", String(page)); //Send web page
}
}
}else{
server.send(200,"text/html","Spotify setup complete");
}
}
long timeLoop;
long refreshLoop;
bool serverOn = true;
/*==============
|Setup function|
==============*/
void setup(){
Serial.begin(115200);
WiFi.begin(WIFI_SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected to WiFi ");
Serial.println("Ip is ");
Serial.println(WiFi.localIP());
server.on("/", handleRoot); //Which routine to handle at root location
server.on("/callback", handleCallbackPage); //Which routine to handle at root location
server.begin(); //Start server
Serial.println("HTTP server started");
for(int i = 0 ; i < 4; i++){
pinMode(buttonPins[i],INPUT_PULLUP);
}
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
delay(1000);
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 10);
display.println(WiFi.localIP());
display.display();
}
// * Sets up WiFi
// * Shows Ip on screen
// * Goes through spotify API handshake (SpotConn func)
// * Initializes screen
// * Checks to see if anything is currently playing (SpotCon func)
// * Shows cute face if needed
void loop(){
if(spotifyConnection.accessTokenSet){
if(serverOn){
server.close();
serverOn = false;
}
if((millis() - spotifyConnection.tokenStartTime)/1000 > spotifyConnection.tokenExpireTime){
Serial.println("refreshing token");
if(spotifyConnection.refreshAuth()){
Serial.println("refreshed token");
}
}
if((millis() - refreshLoop) > 5000){
spotifyConnection.getTrackInfo();
spotifyConnection.drawScreen();
Serial.println("Screen refreshed");
refreshLoop = millis();
}
for(int i = 0; i < 4; i ++){
int reading = digitalRead(buttonPins[i]);
if( reading != buttonStates[i]){
if(millis() - debounceTimes[i] > debounceDelay){
buttonStates[i] = reading;
if(reading == LOW){
switch (i)
{
case 0:
Serial.println("Button play pressed");
spotifyConnection.togglePlay();
break;
case 1:
Serial.println("Button like pressed");
spotifyConnection.toggleLiked(spotifyConnection.currentSong.Id);
break;
case 2:
Serial.println("Button forward pressed");
spotifyConnection.skipForward();
break;
case 3:
Serial.println("Button backward pressed");
spotifyConnection.skipBack();
break;
default:
break;
}
}
debounceTimes[i] = millis();
}
}
}
//int volRequest = map(analogRead(18),0,1023,0,100);
int volRequest = 50;
if(abs(volRequest - spotifyConnection.currVol) > 2){
spotifyConnection.adjustVolume(volRequest);
}
timeLoop = millis();
}else{
server.handleClient();
}
// Serial.println(millis() - timeLoop);
// timeLoop = millis();
}
//Loop function
// *Check for spotify connection refresh
// *Check inputs
// *update screen