#include "SpotifyESP32.h" namespace Spotify_types { bool SHUFFLE_ON = true; bool SHUFFLE_OFF = false; char* REPEAT_OFF = "off"; char* REPEAT_TRACK = "track"; char* REPEAT_CONTEXT = "context"; char* TYPE_ALBUM = "album"; char* TYPE_ARTIST = "artist"; char* TYPE_TRACK = "track"; char* TYPE_PLAYLIST = "playlist"; char* TOP_TYPE_ARTIST = "artists"; char* TOP_TYPE_TRACKS = "tracks"; char* GROUP_ALBUM = "album"; char* GROUP_SINGLE = "single"; char* GROUP_APPEARS_ON = "appears_on"; char* GROUP_COMPILATION = "compilation"; char* TIME_RANGE_SHORT = "short_term"; char* TIME_RANGE_MEDIUM = "medium_term"; char* TIME_RANGE_LONG = "long_term"; int SIZE_OF_ID = 40; int SIZE_OF_URI = 50; } Spotify::Spotify(const char* client_id,const char* client_secret,int server_port, bool debug_on, int max_num_retry): _server(server_port){ _retry = 0; _client_id = client_id; _client_secret = client_secret; _port = server_port; _debug_on = debug_on; if(max_num_retry>0){ _max_num_retry = max_num_retry; } else{ _max_num_retry = 1; } } Spotify::Spotify(const char* client_id,const char* client_secret,const char* refresh_token, bool debug_on, int max_num_retry) { _port = 80; _retry = 0; _client_id = client_id; _client_secret = client_secret; strcpy(_refresh_token, refresh_token); _debug_on = debug_on; if(max_num_retry>0){ _max_num_retry = max_num_retry; } else{ _max_num_retry = 1; } } std::function callback_fn(Spotify *spotify) { return [spotify]() { return spotify->callback_login_page(); }; }; void Spotify::callback_login_page() { if (strcmp(_refresh_token, "") == 0) { if (_server.args() == 0) { char page[900]; snprintf(page,sizeof(page),_login_page, _client_id, _redirect_uri); _server.send(200, "text/html", String(page)); } else { if (_server.hasArg("code")) { _server.send(200, "text/html", "Setup Complete
You can now close this page"); strncpy(_refresh_token, _server.arg("code").c_str(), sizeof(_refresh_token)); if(_debug_on){ Serial.printf("Refresh token: %s\n", _refresh_token); } _server.stop(); } else { char page[900]; snprintf(page,sizeof(page),_login_page, _client_id, _redirect_uri); _server.send(200, "text/html", String(page)); } } } else { _server.send(200, "text/html", "Spotify setup complete"); _server.stop(); } } void Spotify::begin(){ if(strcmp(_refresh_token, "") == 0){ if(_port == 80){ sprintf(_redirect_uri, "http://%s/", WiFi.localIP().toString().c_str()); Serial.printf("Go to this url in your Browser to login to spotify: %s\n", _redirect_uri); } else{ sprintf(_redirect_uri, "http://%s:%d/", WiFi.localIP().toString().c_str(), _port); Serial.printf("Go to this url in your Browser to login to spotify: %s\n", _redirect_uri); } _server.on("/", HTTP_GET, [this](){ if(_debug_on){ Serial.println("Send response to Root"); } auto callback = callback_fn(this); callback(); }); _server.begin(); if(_debug_on){ Serial.println("Server started"); } } else{ if(_debug_on){ Serial.println("Refresh token already set"); } } } void Spotify::handle_client(){ _server.handleClient(); } bool Spotify::is_auth(){ return strcmp(_refresh_token, "") != 0; } //Basic functions response Spotify::RestApiPut(char* rest_url,int payload_size, char* payload){ response response_obj; init_response(&response_obj); HTTPClient http; http.begin(rest_url,_spotify_root_ca); http.addHeader("Accept", "application/json"); http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization","Bearer "+_access_token); http.addHeader("content-Length", String(payload_size)); int http_code=http.PUT(payload); response_obj.status_code = http_code; String reply = ""; DynamicJsonDocument doc(2000); if(http.getSize()>0){ reply = http.getString(); deserializeJson(doc, reply); } if(_debug_on){ const char* endpoint = extract_endpoint(rest_url); Serial.printf("PUT \"%s\" status: ", endpoint); Serial.println(http_code); Serial.print(" Reply: "); Serial.println(reply); } if ((http_code >= 200)&&(http_code <= 299)){ response_obj.reply = "Success"; } else if(_retry<=_max_num_retry){ String message = doc["error"]["message"].as(); if(message == "Only valid bearer authentication supported"){ _retry++; if(get_token()){ response_obj = RestApiPut(rest_url,payload_size, payload); } } else{ response_obj.reply = message; } } http.end(); _retry = 0; return response_obj; } response Spotify::RestApiGet(char* rest_url){ response response_obj; init_response(&response_obj); HTTPClient http; http.begin(rest_url,_spotify_root_ca); http.addHeader("Accept", "application/json"); http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization","Bearer "+_access_token); int http_code = http.GET(); response_obj.status_code = http_code; if(_debug_on){ const char* endpoint = extract_endpoint(rest_url); Serial.printf("GET \"%s\" status: ", endpoint); Serial.println(http_code); Serial.print("Reply: "); Serial.println(http.getString()); } if ((http_code >=200)&&(http_code<=299)){ String response = http.getString(); response_obj.reply = response; } else if(_retry<=_max_num_retry){ _retry++; if(get_token()){ response_obj = RestApiGet(rest_url); } } http.end(); _retry = 0; return response_obj; } response Spotify::RestApiPost(char* rest_url,int payload_size, char* payload){ response response_obj; init_response(&response_obj); HTTPClient http; http.begin(rest_url,_spotify_root_ca); http.addHeader("Accept", "application/json"); http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization","Bearer "+_access_token); http.addHeader("content-Length", String(payload_size)); int http_code=http.POST(payload); response_obj.status_code = http_code; String reply = ""; DynamicJsonDocument doc(2000); if(http.getSize()>0){ reply = http.getString(); deserializeJson(doc, reply); } if(_debug_on){ const char* endpoint = extract_endpoint(rest_url); Serial.printf("POST \"%s\" status: ", endpoint); Serial.println(http_code); Serial.print(" Reply: "); Serial.println(reply); } if ((http_code >= 200)&&(http_code <= 299)){ response_obj.reply = "Success"; } else if(_retry<=_max_num_retry){ String message = doc["error"]["message"].as(); if(message == "Only valid bearer authentication supported"){ _retry++; if(get_token()){ response_obj = RestApiPost(rest_url,payload_size, payload); } } else{ response_obj.reply = message; } } http.end(); _retry = 0; return response_obj; } response Spotify::RestApiDelete(char* rest_url, char* payload){ response response_obj; init_response(&response_obj); HTTPClient http; http.begin(rest_url, _spotify_root_ca); http.addHeader("Accept", "application/json"); http.addHeader("Content-Type", "application/json"); http.addHeader("Authorization", "Bearer " + _access_token); int http_code = http.sendRequest("DELETE", payload); response_obj.status_code = http_code; String reply = ""; DynamicJsonDocument doc(2000); if (http.getSize() > 0) { reply = http.getString(); deserializeJson(doc, reply); } if (_debug_on) { const char* endpoint = extract_endpoint(rest_url); Serial.printf("DELETE \"%s\" status: ", endpoint); Serial.println(http_code); Serial.print(" Reply: "); Serial.println(reply); } if ((http_code >= 200) && (http_code <= 299)) { response_obj.reply = "Success"; } else if (_retry <= _max_num_retry) { String message = doc["error"]["message"].as(); if (message == "Only valid bearer authentication supported") { _retry++; if (get_token()) { response_obj = RestApiDelete(rest_url, payload); } } else { response_obj.reply = message; } } http.end(); _retry = 0; return response_obj; } bool Spotify::get_token(){ bool reply = false; HTTPClient http; String url = "https://accounts.spotify.com/api/token"; String authorization = String(_client_id) + ":" + String(_client_secret); authorization.trim(); authorization = "Basic " + base64::encode(authorization); http.begin(url,_spotify_root_ca); http.addHeader("Content-Type", "application/x-www-form-urlencoded"); http.addHeader("Authorization", authorization); String payload = "grant_type=refresh_token&refresh_token=" + String(_refresh_token); int http_code = http.POST(payload); if (_debug_on) { Serial.print("POST \"token\" status: "); Serial.println(http_code); } if ((http_code >=200)&&(http_code<=299)) { String response = http.getString(); StaticJsonDocument<500> doc; deserializeJson(doc, response); _access_token = doc["access_token"].as(); reply = true; } else{ reply = false; } http.end(); return reply; } void Spotify::init_response(response* response_obj){ response_obj -> status_code = -1; response_obj -> reply ="If you see this something went wrong"; } char* Spotify::array_to_char(int size, char** array, char* result) { result[0] = '\0'; for (int i = 0; i < size; ++i) { strcat(result, array[i]); if (i != size - 1) { strcat(result, ","); } } return result; } void Spotify::array_to_json_array(int size, char** array, char* data, int data_size){ DynamicJsonDocument doc(_max_char_size); JsonArray json_array = doc.to(); for (int i = 0; i char_params; std::map float_params; populate_float_values(float_params, recom); populate_char_values(char_params, recom); for (const auto& param : char_params) { char value[100]; sprintf(value, "&%s%s",param.first, param.second); strcat(url, value); } for(const auto& param : float_params){ char value[100]; sprintf(value, "&%s=%f",param.first, param.second); strcat(url, value); } return RestApiGet(url); } bool Spotify::is_valid_value(float param) { return param >= 0.0 && param <= 1.0; } bool Spotify::is_valid_value(int param) { return param >0; } void Spotify::populate_float_values(std::map& float_params, recommendations& recom){ if (is_valid_value(recom.min_acousticness)) { float_params["min_acousticness"] = recom.min_acousticness; } if (is_valid_value(recom.max_acousticness)) { float_params["max_acousticness"] = recom.max_acousticness; } if (is_valid_value(recom.target_acousticness)) { float_params["target_acousticness"] = recom.target_acousticness; } if (is_valid_value(recom.min_danceability)) { float_params["min_danceability"] = recom.min_danceability; } if (is_valid_value(recom.max_danceability)) { float_params["max_danceability"] = recom.max_danceability; } if (is_valid_value(recom.target_danceability)) { float_params["target_danceability"] = recom.target_danceability; } if (is_valid_value(recom.min_duration_ms)) { float_params["min_duration_ms"] = recom.min_duration_ms; } if (is_valid_value(recom.max_duration_ms)) { float_params["max_duration_ms"] = recom.max_duration_ms; } if (is_valid_value(recom.target_duration_ms)) { float_params["target_duration_ms"] = recom.target_duration_ms; } if (is_valid_value(recom.min_energy)) { float_params["min_energy"] = recom.min_energy; } if (is_valid_value(recom.max_energy)) { float_params["max_energy"] = recom.max_energy; } if (is_valid_value(recom.target_energy)) { float_params["target_energy"] = recom.target_energy; } if (is_valid_value(recom.min_instrumentalness)) { float_params["min_instrumentalness"] = recom.min_instrumentalness; } if (is_valid_value(recom.max_instrumentalness)) { float_params["max_instrumentalness"] = recom.max_instrumentalness; } if (is_valid_value(recom.target_instrumentalness)) { float_params["target_instrumentalness"] = recom.target_instrumentalness; } if (is_valid_value(recom.min_key)) { float_params["min_key"] = recom.min_key; } if (is_valid_value(recom.max_key)) { float_params["max_key"] = recom.max_key; } if (is_valid_value(recom.target_key)) { float_params["target_key"] = recom.target_key; } if (is_valid_value(recom.min_liveness)) { float_params["min_liveness"] = recom.min_liveness; } if (is_valid_value(recom.max_liveness)) { float_params["max_liveness"] = recom.max_liveness; } if (is_valid_value(recom.target_liveness)) { float_params["target_liveness"] = recom.target_liveness; } if (is_valid_value(recom.min_loudness)) { float_params["min_loudness"] = recom.min_loudness; } if (is_valid_value(recom.max_loudness)) { float_params["max_loudness"] = recom.max_loudness; } if (is_valid_value(recom.target_loudness)) { float_params["target_loudness"] = recom.target_loudness; } if (is_valid_value(recom.min_mode)) { float_params["min_mode"] = recom.min_mode; } if (is_valid_value(recom.max_mode)) { float_params["max_mode"] = recom.max_mode; } if (is_valid_value(recom.target_mode)) { float_params["target_mode"] = recom.target_mode; } if (is_valid_value(recom.min_popularity)) { float_params["min_popularity"] = recom.min_popularity; } if (is_valid_value(recom.max_popularity)) { float_params["max_popularity"] = recom.max_popularity; } if (is_valid_value(recom.target_popularity)) { float_params["target_popularity"] = recom.target_popularity; } if (is_valid_value(recom.min_speechiness)) { float_params["min_speechiness"] = recom.min_speechiness; } if (is_valid_value(recom.max_speechiness)) { float_params["max_speechiness"] = recom.max_speechiness; } if (is_valid_value(recom.target_speechiness)) { float_params["target_speechiness"] = recom.target_speechiness; } if (is_valid_value(recom.min_tempo)) { float_params["min_tempo"] = recom.min_tempo; } if (is_valid_value(recom.max_tempo)) { float_params["max_tempo"] = recom.max_tempo; } if (is_valid_value(recom.target_tempo)) { float_params["target_tempo"] = recom.target_tempo; } if (is_valid_value(recom.min_time_signature)) { float_params["min_time_signature"] = recom.min_time_signature; } if (is_valid_value(recom.max_time_signature)) { float_params["max_time_signature"] = recom.max_time_signature; } if (is_valid_value(recom.target_time_signature)) { float_params["target_time_signature"] = recom.target_time_signature; } if (is_valid_value(recom.min_valence)) { float_params["min_valence"] = recom.min_valence; } if (is_valid_value(recom.max_valence)) { float_params["max_valence"] = recom.max_valence; } if (is_valid_value(recom.target_valence)) { float_params["target_valence"] = recom.target_valence; } } void Spotify::populate_char_values(std::map& char_params, recommendations& recom){ char arr[_max_char_size]; if(is_valid_value(recom.seed_artists_size)){ char_params["seed_artists"] = array_to_char(recom.seed_artists_size, recom.seed_artists, arr); } if(is_valid_value(recom.seed_genres_size)){ char_params["seed_genres"] = array_to_char(recom.seed_genres_size, recom.seed_genres, arr); } if(is_valid_value(recom.seed_tracks_size)){ char_params["seed_tracks"] = array_to_char(recom.seed_tracks_size, recom.seed_tracks,arr); } } #endif #ifdef ENABLE_USER //Users response Spotify::get_current_user_profile() { char url[] = "https://api.spotify.com/v1/me"; return RestApiGet(url); } response Spotify::get_user_top_items(char* type, char* time_range, int limit, int offset) { char url[100]; sprintf(url, "https://api.spotify.com/v1/me/top/%s?time_range=%s&limit=%d&offset=%d", type, time_range, limit, offset); return RestApiGet(url); } response Spotify::get_user_profile(char* user_id) { char url[100]; sprintf(url, "https://api.spotify.com/v1/users/%s", user_id); return RestApiGet(url); } response Spotify::follow_playlist(char* playlist_id, bool is_public) { char url[100]; sprintf(url, "https://api.spotify.com/v1/playlists/%s/followers", playlist_id); char payload[100]; int payload_size = 0; sprintf(payload, "{\"public\":%s}", is_public ? "true" : "false"); payload_size = strlen(payload); return RestApiPut(url, payload_size, payload); } response Spotify::unfollow_playlist(char* playlist_id) { char url[100]; sprintf(url, "https://api.spotify.com/v1/playlists/%s/followers", playlist_id); return RestApiDelete(url); } response Spotify::get_followed_artists(char* type, char* after, int limit) { char url[100]; sprintf(url, "https://api.spotify.com/v1/me/following?type=%s&after=%s&limit=%d", type, after, limit); return RestApiGet(url); } response Spotify::follow_artists_or_users(char* type,int size, char** artist_user_ids) { char url[100]; sprintf(url, "https://api.spotify.com/v1/me/following?type=%s", type); char payload[_max_char_size]; int payload_size = 0; array_to_json_array(size, artist_user_ids, payload); payload_size = strlen(payload); return RestApiPut(url, payload_size, payload); } response Spotify::unfollow_artists_or_users(char* type,int size, char** artist_user_ids) { char url[100]; sprintf(url, "https://api.spotify.com/v1/me/following?type=%s", type); char payload[_max_char_size]; array_to_json_array(size, artist_user_ids, payload); return RestApiDelete(url, payload); } response Spotify::check_if_user_follows_artists_or_users(char* type,int size, char** artist_user_ids) { char url[_max_char_size]; char arr[_max_char_size]; sprintf(url, "https://api.spotify.com/v1/me/following/contains?type=%s&ids=%s", type, array_to_char(size, artist_user_ids, arr)); return RestApiGet(url); } response Spotify::check_if_users_follow_playlist(char* playlist_id,int size, char** user_ids) { char url[_max_char_size]; char arr[_max_char_size]; sprintf(url, "https://api.spotify.com/v1/playlists/%s/followers/contains?ids=%s", playlist_id, array_to_char(size, user_ids, arr)); return RestApiGet(url); } #endif #ifdef ENABLE_SIMPIFIED //Simplified functions, formatting functions String Spotify::current_track_name(){ String track_name = "Something went wrong"; response data = currently_playing(); if((data.status_code>=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(10000); deserializeJson(doc,data.reply); track_name = doc["item"]["name"].as(); } return track_name; } String Spotify::current_track_id(){ String track_id = "Something went wrong"; response data = currently_playing(); if((data.status_code>=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(10000); deserializeJson(doc,data.reply); track_id = doc["item"]["id"].as(); } return track_id; } String Spotify::current_device_id(){ String device_id = "Something went wrong"; response data = available_devices(); if((data.status_code>=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(2000); deserializeJson(doc,data.reply); JsonArray devices = doc["devices"].as(); for (JsonVariant device : devices) { JsonObject deviceObj = device.as(); if (deviceObj["is_active"].as()) { device_id = deviceObj["id"].as(); break; } } } return device_id; } String Spotify::current_artist_names(){ String artist_names = "Something went wrong"; response data = currently_playing(); if((data.status_code>=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(10000); deserializeJson(doc,data.reply); JsonArray array = doc["item"]["artists"]; int len = array.size(); artist_names = ""; for (int i = 0; i(); if (i=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(2000); deserializeJson(doc,data.reply); JsonArray devices = doc["devices"].as(); for (JsonVariant device : devices) { JsonObject deviceObj = device.as(); if (deviceObj["is_active"].as()) { sprintf(device_id, "%s", deviceObj["id"].as().c_str()); break; } } } return device_id; } char* Spotify::current_track_name(char * track_name){ response data = currently_playing(); if((data.status_code>=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(10000); deserializeJson(doc,data.reply); sprintf(track_name, "%s", doc["item"]["name"].as().c_str()); } return track_name; } char* Spotify::current_track_id(char * track_id){ response data = currently_playing(); if((data.status_code>=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(10000); deserializeJson(doc,data.reply); sprintf(track_id, "%s", doc["item"]["id"].as().c_str()); } return track_id; } char* Spotify::current_artist_names(char * artist_names){ response data = currently_playing(); if((data.status_code>=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(10000); deserializeJson(doc,data.reply); JsonArray array = doc["item"]["artists"]; int len = array.size(); artist_names[0] = '\0'; for (int i = 0; i().c_str()); if (i=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(10000); deserializeJson(doc,data.reply); is_playing = doc["is_playing"].as(); } return is_playing; } bool Spotify::volume_modifyable(){ bool volume_modifyable = false; response data = current_playback_state(); if((data.status_code>=200)&&(data.status_code<=299)){ DynamicJsonDocument doc(10000); deserializeJson(doc,data.reply); volume_modifyable = doc["device"]["supports_volume"]; } return volume_modifyable; } #endif char Spotify::convert_id_to_uri(char* id, char* type){ char uri[45]; sprintf(uri, "spotify:%s:%s", type, id); return *uri; } char* Spotify::convert_id_to_uri(char* id, char* type,char * uri){ sprintf(uri, "spotify:%s:%s", type, id); return uri; } void print_response(response response_obj){ Serial.printf("Status: %d\n", response_obj.status_code); Serial.printf("Reply: %s\n", response_obj.reply); }