#include #include #include "spotify_api.h" const int HTTP_RESPONSE_CODE_UNAUTHORIZED = 401; const int HTTP_RESPONSE_CODE_OK = 200; const int HTTP_RESPONSE_CODE_NO_CONTENT = 204; const char *spotifyApiUrl = "https://api.spotify.com/v1"; SpotifyApi::SpotifyApi(String cid, String cs, String ac, String rt) { this->clientId = cid; this->clientSecret = cs; this->accessToken = ac; this->refreshToken = rt; this->lastHttpResponseCode = 0; } void SpotifyApi::withAuth(HTTPClient &http) { http.addHeader("Authorization", "Bearer " + accessToken); } String SpotifyApi::refreshAccessToken() { HTTPClient http; http.begin("https://accounts.spotify.com/api/token"); http.addHeader("Content-Type", "application/x-www-form-urlencoded"); String httpRequestData = "grant_type=refresh_token&refresh_token=" + refreshToken + "&client_id=" + clientId + "&client_secret=" + clientSecret; int httpResponseCode = http.POST(httpRequestData); lastHttpResponseCode = httpResponseCode; if (httpResponseCode != HTTP_RESPONSE_CODE_OK) { Serial.println("Got error " + String(httpResponseCode)); Serial.println("Failed to refresh access token"); return ""; } String response = http.getString(); http.end(); DynamicJsonDocument doc(1024); deserializeJson(doc, response); return doc["access_token"].as(); } bool SpotifyApi::performHttpRequestWithRetry(HTTPClient &http, String &apiUrl, String &response) { accessToken = SpotifyApi::refreshAccessToken(); if (accessToken == "") { Serial.println("Failed to refresh access token"); return false; } http.begin(apiUrl); SpotifyApi::withAuth(http); int httpResponseCode = http.GET(); lastHttpResponseCode = httpResponseCode; if (httpResponseCode > 0) { response = http.getString(); return true; } else { Serial.println("HTTP request failed with error: " + String(httpResponseCode)); return false; } } TrackInfo SpotifyApi::getCurrentTrackInfo() { HTTPClient http; String apiUrl = String(spotifyApiUrl) + "/me/player/currently-playing"; http.begin(apiUrl); SpotifyApi::withAuth(http); TrackInfo trackInfo; String response; int httpResponseCode = http.GET(); lastHttpResponseCode = httpResponseCode; if (httpResponseCode == HTTP_RESPONSE_CODE_UNAUTHORIZED) { if (performHttpRequestWithRetry(http, apiUrl, response)) { SpotifyApi::errorCount = 0; DynamicJsonDocument doc(1024); deserializeJson(doc, response); trackInfo.name = doc["item"]["name"].as(); JsonArray artists = doc["item"]["artists"].as(); String artistNames = ""; for (JsonVariant artist : artists) { if (artistNames.length() > 0) { artistNames += ", "; } artistNames += artist["name"].as(); } trackInfo.artistName = artistNames; trackInfo.coverUrl = doc["item"]["album"]["images"][2]["url"].as(); trackInfo.duration = doc["item"]["duration_ms"].as(); trackInfo.progress = doc["progress_ms"].as(); trackInfo.isPlaying = doc["is_playing"].as(); } else { SpotifyApi::errorCount++; } } else if (httpResponseCode > 0) { response = http.getString(); SpotifyApi::errorCount = 0; DynamicJsonDocument doc(1024); deserializeJson(doc, response); trackInfo.name = doc["item"]["name"].as(); trackInfo.artistName = doc["item"]["artists"][0]["name"].as(); trackInfo.coverUrl = doc["item"]["album"]["images"][2]["url"].as(); trackInfo.duration = doc["item"]["duration_ms"].as(); trackInfo.progress = doc["progress_ms"].as(); trackInfo.isPlaying = doc["is_playing"].as(); } else { Serial.println("HTTP request failed with error: " + String(httpResponseCode)); SpotifyApi::errorCount++; } http.end(); return trackInfo; } std::tuple SpotifyApi::getDevicesList() { HTTPClient http; String apiUrl = String(spotifyApiUrl) + "/me/player/devices"; http.begin(apiUrl); SpotifyApi::withAuth(http); String response; int httpResponseCode = http.GET(); lastHttpResponseCode = httpResponseCode; if (httpResponseCode == HTTP_RESPONSE_CODE_UNAUTHORIZED) { if (!performHttpRequestWithRetry(http, apiUrl, response)) { Serial.println("Failed to get devices list"); http.end(); return std::make_tuple(nullptr, 0); } } else if (httpResponseCode <= 0) { Serial.println("HTTP request failed with error: " + String(httpResponseCode)); http.end(); return std::make_tuple(nullptr, 0); } else { response = http.getString(); } DynamicJsonDocument doc(2048); deserializeJson(doc, response); JsonArray devices = doc["devices"].as(); Device *deviceList = new (std::nothrow) Device[devices.size()]; if (deviceList == nullptr) { Serial.println("Failed to allocate memory for device list"); http.end(); return std::make_tuple(nullptr, 0); } int index = 0; for (JsonVariant device : devices) { deviceList[index].id = device["id"].as(); deviceList[index].name = device["name"].as(); index++; } http.end(); Serial.print("Number of devices: "); Serial.println(devices.size()); return std::make_tuple(deviceList, devices.size()); } void SpotifyApi::setActiveDevice(String deviceId) { HTTPClient http; String apiUrl = String(spotifyApiUrl) + "/me/player"; http.begin(apiUrl); SpotifyApi::withAuth(http); http.addHeader("Content-Type", "application/json"); String payload = "{\"device_ids\":[\"" + String(deviceId) + "\"]}"; int httpResponseCode = http.PUT(payload); lastHttpResponseCode = httpResponseCode; if (httpResponseCode == HTTP_RESPONSE_CODE_UNAUTHORIZED) { String response; if (performHttpRequestWithRetry(http, apiUrl, response)) { Serial.println("Successfully set the active device."); } else { Serial.printf("Failed to set active device, error: %d with id: %s \n", httpResponseCode, deviceId); } } else if (httpResponseCode != HTTP_RESPONSE_CODE_NO_CONTENT) { Serial.printf("Failed to set active device, error: %d with id: %s \n", httpResponseCode, deviceId); } else { Serial.println("Successfully set the active device."); } http.end(); } String SpotifyApi::getUsername() { HTTPClient http; String apiUrl = String(spotifyApiUrl) + "/me"; http.begin(apiUrl); SpotifyApi::withAuth(http); String response; int httpResponseCode = http.GET(); lastHttpResponseCode = httpResponseCode; if (httpResponseCode == HTTP_RESPONSE_CODE_UNAUTHORIZED) { if (performHttpRequestWithRetry(http, apiUrl, response)) { DynamicJsonDocument doc(1024); deserializeJson(doc, response); http.end(); return doc["display_name"].as(); } else { Serial.println("Failed to get username"); http.end(); return "Unknown User"; } } else if (httpResponseCode > 0) { response = http.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, response); http.end(); return doc["display_name"].as(); } else { Serial.println("HTTP request failed with error: " + String(httpResponseCode)); http.end(); return "Unknown User"; } } void SpotifyApi::controlSpotify(String command) { HTTPClient http; String apiUrl = String(spotifyApiUrl) + "/me/player/" + command; http.begin(apiUrl); SpotifyApi::withAuth(http); http.addHeader("Content-Type", "application/json"); String response; int httpResponseCode; if (command == "play" || command == "pause") { httpResponseCode = http.PUT("{}"); } else if (command == "next" || command == "previous") { httpResponseCode = http.POST("{}"); } lastHttpResponseCode = httpResponseCode; if (httpResponseCode == HTTP_RESPONSE_CODE_UNAUTHORIZED) { if (!performHttpRequestWithRetry(http, apiUrl, response)) { Serial.printf("Failed to send %s command\n", command.c_str()); } else { Serial.printf("Successfully sent %s command to Spotify\n", command.c_str()); } } else if (httpResponseCode > 0) { Serial.printf("Successfully sent %s command to Spotify\n", command.c_str()); } else { Serial.printf("Failed to send %s command\n", command.c_str()); } http.end(); } void SpotifyApi::setVolume(int volume) { HTTPClient http; String apiUrl = String(spotifyApiUrl) + "/me/player/volume?volume_percent=" + String(volume); http.begin(apiUrl); SpotifyApi::withAuth(http); http.addHeader("Content-Type", "application/json"); int httpResponseCode = http.PUT(""); lastHttpResponseCode = httpResponseCode; if (httpResponseCode == HTTP_RESPONSE_CODE_UNAUTHORIZED) { String response; if (!performHttpRequestWithRetry(http, apiUrl, response)) { Serial.printf("Failed to set volume to %d\n", volume); } else { Serial.printf("Successfully set volume to %d\n", volume); } } else if (httpResponseCode > 0) { Serial.printf("Successfully set volume to %d\n", volume); } else { Serial.printf("Failed to set volume to %d\n", volume); } http.end(); } std::tuple SpotifyApi::getFeaturedPlaylists(int limit, int offset) { HTTPClient http; String apiUrl = String(spotifyApiUrl) + "/browse/featured-playlists?limit=" + String(limit) + "&offset=" + String(offset) + "&locale=en_US"; http.begin(apiUrl); SpotifyApi::withAuth(http); String response; int httpResponseCode = http.GET(); lastHttpResponseCode = httpResponseCode; if (httpResponseCode == HTTP_RESPONSE_CODE_UNAUTHORIZED) { if (!performHttpRequestWithRetry(http, apiUrl, response)) { Serial.println("Failed to get featured playlists"); http.end(); return std::make_tuple(nullptr, 0); } } else if (httpResponseCode <= 0) { Serial.println("HTTP request failed with error: " + String(httpResponseCode)); http.end(); return std::make_tuple(nullptr, 0); } else { response = http.getString(); } DynamicJsonDocument doc(2048); deserializeJson(doc, response); JsonArray playlists = doc["playlists"]["items"].as(); FeaturedPlaylist *playlistList = new (std::nothrow) FeaturedPlaylist[playlists.size()]; if (playlistList == nullptr) { Serial.println("Failed to allocate memory for playlist list"); http.end(); return std::make_tuple(nullptr, 0); } int index = 0; for (JsonVariant playlist : playlists) { playlistList[index].name = playlist["name"].as(); playlistList[index].id = playlist["id"].as(); playlistList[index].imageUrl = playlist["images"][2]["url"].as(); index++; } http.end(); return std::make_tuple(playlistList, playlists.size()); } void SpotifyApi::playPlaylist(String playlistId) { HTTPClient http; String apiUrl = String(spotifyApiUrl) + "/me/player/play"; http.begin(apiUrl); SpotifyApi::withAuth(http); http.addHeader("Content-Type", "application/json"); String payload = "{\"context_uri\":\"spotify:playlist:" + playlistId + "\"}"; int httpResponseCode = http.PUT(payload); lastHttpResponseCode = httpResponseCode; if (httpResponseCode == HTTP_RESPONSE_CODE_UNAUTHORIZED) { String response; if (!performHttpRequestWithRetry(http, apiUrl, response)) { Serial.println("Failed to play playlist"); } } else if (httpResponseCode <= 0) { Serial.println("HTTP request failed with error: " + String(httpResponseCode)); } http.end(); } std::tuple SpotifyApi::getUserPlaylists(int limit, int offset) { HTTPClient http; String apiUrl = String(spotifyApiUrl) + "/me/playlists?limit=" + String(limit) + "&offset=" + String(offset); http.begin(apiUrl); withAuth(http); int httpResponseCode = http.GET(); this->lastHttpResponseCode = httpResponseCode; if (httpResponseCode != HTTP_CODE_OK) { Serial.println("Failed to get user playlists, error: " + String(httpResponseCode)); http.end(); return std::make_tuple(nullptr, 0); } String response = http.getString(); DynamicJsonDocument doc(1024); deserializeJson(doc, response); JsonArray playlists = doc["items"].as(); size_t playlistCount = playlists.size(); FeaturedPlaylist *playlistList = new FeaturedPlaylist[playlistCount]; if (playlistList == nullptr) { Serial.println("Failed to allocate memory for playlist list"); http.end(); return std::make_tuple(nullptr, 0); } int index = 0; for (JsonVariant playlist : playlists) { playlistList[index].name = playlist["name"].as(); playlistList[index].id = playlist["id"].as(); playlistList[index].imageUrl = playlist["images"][0]["url"].as(); index++; } http.end(); return std::make_tuple(playlistList, playlistCount); }