├── .gitignore ├── .travis.yml ├── FUNDING.yml ├── LICENSE ├── README.md ├── examples ├── displayAlbumArt │ └── tftAlbumArt │ │ └── tftDisplayImage.ino ├── getCurrentlyPlaying │ └── getCurrentlyPlaying.ino ├── getDevices │ └── getDevices.ino ├── getRefreshToken │ └── getRefreshToken.ino ├── getSearchResults │ └── getSearchResults.ino ├── playAdvanced │ └── playAdvanced.ino ├── playerControls │ └── playerControls.ino └── playerDetails │ └── playerDetails.ino ├── library.json ├── library.properties ├── platformio.ini ├── scripts └── travis │ ├── platformio.sh │ └── platformioSingle.sh └── src ├── SpotifyArduino.cpp ├── SpotifyArduino.h └── SpotifyArduinoCert.h /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .vscode 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | 5 | # Cache PlatformIO packages using Travis CI container-based infrastructure 6 | cache: 7 | directories: 8 | - "~/.platformio" 9 | 10 | env: 11 | # ESP8266 12 | - SCRIPT=platformioSingle EXAMPLE_NAME=getCurrentlyPlaying EXAMPLE_FOLDER=/ BOARD=d1_mini 13 | - SCRIPT=platformioSingle EXAMPLE_NAME=getRefreshToken EXAMPLE_FOLDER=/ BOARD=d1_mini 14 | - SCRIPT=platformioSingle EXAMPLE_NAME=playAdvanced EXAMPLE_FOLDER=/ BOARD=d1_mini 15 | - SCRIPT=platformioSingle EXAMPLE_NAME=playerControls EXAMPLE_FOLDER=/ BOARD=d1_mini 16 | - SCRIPT=platformioSingle EXAMPLE_NAME=playerDetails EXAMPLE_FOLDER=/ BOARD=d1_mini 17 | - SCRIPT=platformioSingle EXAMPLE_NAME=getDevices EXAMPLE_FOLDER=/ BOARD=d1_mini 18 | 19 | # ESP32 20 | # - SCRIPT=platformioSingle EXAMPLE_NAME=albumArtMatrix EXAMPLE_FOLDER=/displayAlbumArt/ BOARDTYPE=ESP32 BOARD=esp32dev 21 | - SCRIPT=platformioSingle EXAMPLE_NAME=getCurrentlyPlaying EXAMPLE_FOLDER=/ BOARD=esp32dev 22 | - SCRIPT=platformioSingle EXAMPLE_NAME=getRefreshToken EXAMPLE_FOLDER=/ BOARD=esp32dev 23 | - SCRIPT=platformioSingle EXAMPLE_NAME=playAdvanced EXAMPLE_FOLDER=/ BOARD=esp32dev 24 | - SCRIPT=platformioSingle EXAMPLE_NAME=playerControls EXAMPLE_FOLDER=/ BOARD=esp32dev 25 | - SCRIPT=platformioSingle EXAMPLE_NAME=playerDetails EXAMPLE_FOLDER=/ BOARD=esp32dev 26 | - SCRIPT=platformioSingle EXAMPLE_NAME=getDevices EXAMPLE_FOLDER=/ BOARD=esp32dev 27 | 28 | before_install: 29 | 30 | install: 31 | - pip install -U platformio 32 | - platformio update 33 | # 34 | # Libraries from PlatformIO Library Registry: 35 | # 36 | # https://platformio.org/lib/show/64/ArduinoJson 37 | # https://platformio.org/lib/show/3577/ESP32%2064x32%20LED%20MATRIX%20HUB75%20DMA%20Display 38 | # https://platformio.org/lib/show/13/Adafruit%20GFX%20Library 39 | # https://platformio.org/lib/show/6906/TJpg_Decoder 40 | # https://platformio.org/lib/show/6214/Adafruit%20BusIO 41 | - platformio lib -g install 64 3577 13 6906 6214 42 | 43 | before_script: 44 | # 45 | 46 | script: 47 | - scripts/travis/$SCRIPT.sh 48 | 49 | before_cache: 50 | # - rm -rf ~/.platformio 51 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [witnessmenow] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Brian Lough 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spotify-api-arduino 2 | 3 | ![Travis CI status](https://api.travis-ci.org/witnessmenow/arduino-spotify-api.svg?branch=master) 4 | ![License](https://img.shields.io/github/license/witnessmenow/spotify-api-arduino) 5 | ![Release stable](https://badgen.net/github/release/witnessmenow/spotify-api-arduino/stable) 6 | Arduino library for integrating with a subset of the [Spotify Web-API](https://developer.spotify.com/documentation/web-api/reference/) (Does not play music) 7 | 8 | **Work in progress library - expect changes!** 9 | 10 | ## Supported Boards: 11 | 12 | ### ESP32 13 | 14 | Working well 15 | 16 | ### ESP8266 17 | 18 | Working well 19 | 20 | ### Other boards - Arduino Wifi Nina (Nano IOT etc) 21 | 22 | Should in theory work, but I have no tested it on them and will not be able to provide support for them. 23 | 24 | ## Help support what I do! 25 | 26 | I have put a lot of effort into creating Arduino libraries that I hope people can make use of. [If you enjoy my work, please consider becoming a Github sponsor!](https://github.com/sponsors/witnessmenow/) 27 | 28 | ## Library Features: 29 | 30 | The Library supports the following features: 31 | 32 | - Get Authentication Tokens 33 | - Getting your currently playing track 34 | - Player Controls: 35 | - Next 36 | - Previous 37 | - Seek 38 | - Play (basic version, basically resumes a paused track) 39 | - Play Advanced (play given song, album, artist) 40 | - Pause 41 | - Set Volume (doesn't seem to work on my phone, works on desktop though) 42 | - Set Repeat Modes 43 | - Toggle Shuffle 44 | - Get Devices 45 | - Search Spotify Library 46 | 47 | ### What needs to be added: 48 | 49 | - Better instructions for how to set up your refresh token. 50 | - Example where refresh token and full operation are handled in same sketch. 51 | 52 | ## Setup Instructions 53 | 54 | ### Spotify Account 55 | 56 | - Sign into the [Spotify Developer page](https://developer.spotify.com/dashboard/login) 57 | - Create a new application. (name it whatever you want) 58 | - You will need to use the "client ID" and "client secret" from this page in your sketches 59 | - You will also need to add a callback URI for authentication process by clicking "Edit Settings", what URI to add will be mentioned in further instructions 60 | 61 | ### Getting Your Refresh Token 62 | 63 | Spotify's Authentication flow requires a webserver to complete, but it's only needed once to get your refresh token. Your refresh token can then be used in all future sketches to authenticate. 64 | 65 | Because the webserver is only needed once, I decided to seperate the logic for getting the Refresh token to it's own example. 66 | 67 | Follow the instructions in the [getRefreshToken example](examples/getRefreshToken/getRefreshToken.ino) to get your token. 68 | 69 | Note: Once you have a refresh token, you can use it on either platform in your sketches, it is not tied to any particular device. 70 | 71 | ### Running 72 | 73 | Take one of the included examples and update it with your WiFi creds, Client ID, Client Secret and the refresh token you just generated. 74 | 75 | ### Scopes 76 | 77 | By default the getRefreshToken examples will include the required scopes, but if you want change them the following info might be useful. 78 | 79 | put a `%20` between the ones you need. 80 | 81 | | Feature | Required Scope | 82 | | ------------------------- | -------------------------- | 83 | | Current Playing Song Info | user-read-playback-state | 84 | | Player Controls | user-modify-playback-state | 85 | 86 | ## Installation 87 | 88 | Download zip from Github and install to the Arduino IDE using that. 89 | 90 | #### Dependancies 91 | 92 | - V6 of Arduino JSON - can be installed through the Arduino Library manager. 93 | 94 | ## Compile flag configuration 95 | 96 | There are some flags that you can set in the `SpotifyArduino.h` that can help with debugging 97 | 98 | ``` 99 | 100 | #define SPOTIFY_DEBUG 1 101 | // Enables extra debug messages on the serial. 102 | // Will be disabled by default when library is released. 103 | // NOTE: Do not use this option on live-streams, it will reveal your private tokens! 104 | 105 | #define SPOTIFY_SERIAL_OUTPUT 1 106 | // Comment out if you want to disable any serial output from this library 107 | // (also comment out DEBUG and PRINT_JSON_PARSE) 108 | 109 | //#define SPOTIFY_PRINT_JSON_PARSE 1 110 | // Prints the JSON received to serial (only use for debugging as it will be slow) 111 | // Requires the installation of ArduinoStreamUtils (https://github.com/bblanchon/ArduinoStreamUtils) 112 | 113 | ``` 114 | -------------------------------------------------------------------------------- /examples/displayAlbumArt/tftAlbumArt/tftDisplayImage.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | Displays Album Art on an 128 x 160 display. ESP32 Only 3 | 4 | There is two approaches to this demoed in this example 5 | - "displayImage" uses a memory buffer, it should be the fastest but possible uses the most memory. 6 | - "displayImageUsingFile" uses a File reference 7 | 8 | All references to SPIFFS are only required for the "displayImageUsingFile" path. 9 | 10 | This example could easily be adapted to any TFT 11 | based screen. 12 | 13 | The library for the display will need to be modified to work 14 | with a 128x160 display: 15 | https://github.com/Bodmer/TFT_eSPI 16 | 17 | NOTE: You need to get a Refresh token to use this example 18 | Use the getRefreshToken example to get it. 19 | 20 | Parts: 21 | ESP32 D1 Mini stlye Dev board* - http://s.click.aliexpress.com/e/C6ds4my 22 | ESP32 I2S Matrix Shield (From my Tindie) = https://www.tindie.com/products/brianlough/esp32-i2s-matrix-shield/ 23 | 128x160 LCD Screen - shorturl.at/elKQV 24 | 25 | *******************************************************************/ 26 | 27 | // ---------------------------- 28 | // Standard Libraries 29 | // ---------------------------- 30 | #include 31 | #include 32 | 33 | #include 34 | #include "SPIFFS.h" 35 | 36 | // ---------------------------- 37 | // Additional Libraries - each one of these will need to be installed. 38 | // ---------------------------- 39 | 40 | #include 41 | #include 42 | 43 | #include 44 | #include 45 | 46 | char ssid[] = "SSID"; // your network SSID (name) 47 | char password[] = "password"; // your network password 48 | 49 | char clientId[] = "56t4373258u3405u43u543"; // Your client ID of your spotify APP 50 | char clientSecret[] = "56t4373258u3405u43u543"; // Your client Secret of your spotify APP (Do Not share this!) 51 | 52 | // Country code, including this is advisable 53 | #define SPOTIFY_MARKET "IE" 54 | 55 | #define SPOTIFY_REFRESH_TOKEN "AAAAAAAAAABBBBBBBBBBBCCCCCCCCCCCDDDDDDDDDDD" 56 | 57 | // including a "spotify_server_cert" variable 58 | // header is included as part of the SpotifyArduino libary 59 | #include 60 | 61 | // file name for where to save the image. 62 | #define ALBUM_ART "/album.jpg" 63 | 64 | // so we can compare and not download the same image if we already have it. 65 | String lastAlbumArtUrl; 66 | 67 | // Variable to hold image info 68 | SpotifyImage smallestImage; 69 | 70 | // so we can store the song name and artist name 71 | char *songName; 72 | char *songArtist; 73 | 74 | WiFiClientSecure client; 75 | SpotifyArduino spotify(client, client_id, client_secret, SPOTIFY_REFRESH_TOKEN); 76 | 77 | // You might want to make this much smaller, so it will update responsively 78 | 79 | unsigned long delayBetweenRequests = 30000; // Time between requests (30 seconds) 80 | unsigned long requestDueTime; // time when request due 81 | 82 | TFT_eSPI tft = TFT_eSPI(); 83 | 84 | // This next function will be called during decoding of the jpeg file to 85 | // render each block to the Matrix. If you use a different display 86 | // you will need to adapt this function to suit. 87 | bool displayOutput(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t *bitmap) 88 | { 89 | // Stop further decoding as image is running off bottom of screen 90 | if (y >= tft.height()) 91 | return 0; 92 | 93 | tft.pushImage(x, y, w, h, bitmap); 94 | 95 | // Return 1 to decode next block 96 | return 1; 97 | } 98 | 99 | void setup() 100 | { 101 | Serial.begin(115200); 102 | 103 | // Initialise SPIFFS, if this fails try .begin(true) 104 | // NOTE: I believe this formats it though it will erase everything on 105 | // spiffs already! In this example that is not a problem. 106 | // I have found once I used the true flag once, I could use it 107 | // without the true flag after that. 108 | 109 | if (!SPIFFS.begin()) 110 | { 111 | Serial.println("SPIFFS initialisation failed!"); 112 | while (1) 113 | yield(); // Stay here twiddling thumbs waiting 114 | } 115 | Serial.println("\r\nInitialisation done."); 116 | 117 | // Start the tft display and set it to black 118 | tft.init(); 119 | tft.fillScreen(TFT_BLACK); 120 | 121 | // The jpeg image can be scaled by a factor of 1, 2, 4, or 8 122 | TJpgDec.setJpgScale(4); 123 | 124 | // The decoder must be given the exact name of the rendering function above 125 | TJpgDec.setCallback(displayOutput); 126 | 127 | // The byte order can be swapped (set true for TFT_eSPI) 128 | TJpgDec.setSwapBytes(true); 129 | 130 | WiFi.mode(WIFI_STA); 131 | WiFi.begin(ssid, password); 132 | Serial.println(""); 133 | 134 | // Wait for connection 135 | while (WiFi.status() != WL_CONNECTED) 136 | { 137 | delay(500); 138 | Serial.print("."); 139 | } 140 | Serial.println(""); 141 | Serial.print("Connected to "); 142 | Serial.println(ssid); 143 | Serial.print("IP address: "); 144 | Serial.println(WiFi.localIP()); 145 | 146 | client.setCACert(spotify_server_cert); 147 | 148 | // If you want to enable some extra debugging 149 | // uncomment the "#define SPOTIFY_DEBUG" in SpotifyArduino.h 150 | 151 | Serial.println("Refreshing Access Tokens"); 152 | if (!spotify.refreshAccessToken()) 153 | { 154 | Serial.println("Failed to get access tokens"); 155 | } 156 | } 157 | 158 | int displayImageUsingFile(char *albumArtUrl) 159 | { 160 | 161 | // In this example I reuse the same filename 162 | // over and over, maybe saving the art using 163 | // the album URI as the name would be better 164 | // as you could save having to download them each 165 | // time, but this seems to work fine. 166 | if (SPIFFS.exists(ALBUM_ART) == true) 167 | { 168 | Serial.println("Removing existing image"); 169 | SPIFFS.remove(ALBUM_ART); 170 | } 171 | 172 | fs::File f = SPIFFS.open(ALBUM_ART, "w+"); 173 | if (!f) 174 | { 175 | Serial.println("file open failed"); 176 | return -1; 177 | } 178 | 179 | // Spotify uses a different cert for the Image server, so we need to swap to that for the call 180 | client.setCACert(spotify_image_server_cert); 181 | bool gotImage = spotify.getImage(albumArtUrl, &f); 182 | 183 | // Swapping back to the main spotify cert 184 | client.setCACert(spotify_server_cert); 185 | 186 | // Make sure to close the file! 187 | f.close(); 188 | 189 | if (gotImage) 190 | { 191 | return TJpgDec.drawFsJpg(0, 0, ALBUM_ART); 192 | } 193 | else 194 | { 195 | return -2; 196 | } 197 | } 198 | 199 | int displayImage(char *albumArtUrl) 200 | { 201 | 202 | uint8_t *imageFile; // pointer that the library will store the image at (uses malloc) 203 | int imageSize; // library will update the size of the image 204 | bool gotImage = spotify.getImage(albumArtUrl, &imageFile, &imageSize); 205 | 206 | if (gotImage) 207 | { 208 | Serial.print("Got Image"); 209 | delay(1); 210 | int jpegStatus = TJpgDec.drawJpg(28, 40, imageFile, imageSize); 211 | free(imageFile); // Make sure to free the memory! 212 | return jpegStatus; 213 | } 214 | else 215 | { 216 | return -2; 217 | } 218 | } 219 | 220 | void printCurrentlyPlayingToSerial(CurrentlyPlaying currentlyPlaying) 221 | { 222 | // Use the details in this method or if you want to store them 223 | // make sure you copy them (using something like strncpy) 224 | // const char* artist = 225 | 226 | // Clear the Text every time a new song is created 227 | tft.fillRect(0, 120, 128, 130, TFT_BLACK); 228 | Serial.println("--------- Currently Playing ---------"); 229 | 230 | Serial.print("Is Playing: "); 231 | if (currentlyPlaying.isPlaying) 232 | { 233 | Serial.println("Yes"); 234 | } 235 | else 236 | { 237 | Serial.println("No"); 238 | } 239 | 240 | Serial.print("Track: "); 241 | Serial.println(currentlyPlaying.trackName); 242 | // Save the song name to a variable 243 | songName = const_cast(currentlyPlaying.trackName); 244 | drawMessage(0, 120, songName); 245 | Serial.print("Track URI: "); 246 | Serial.println(currentlyPlaying.trackUri); 247 | Serial.println(); 248 | 249 | Serial.println("Artists: "); 250 | for (int i = 0; i < currentlyPlaying.numArtists; i++) 251 | { 252 | Serial.print("Name: "); 253 | // Save the song artist name to a variable 254 | Serial.println(currentlyPlaying.artists[i].artistName); 255 | songArtist = const_cast(currentlyPlaying.artists[0].artistName); 256 | drawMessage(0, 130, songArtist); 257 | Serial.print("Artist URI: "); 258 | Serial.println(currentlyPlaying.artists[i].artistUri); 259 | Serial.println(); 260 | } 261 | 262 | Serial.print("Album: "); 263 | Serial.println(currentlyPlaying.albumName); 264 | Serial.print("Album URI: "); 265 | Serial.println(currentlyPlaying.albumUri); 266 | Serial.println(); 267 | 268 | long progress = currentlyPlaying.progressMs; // duration passed in the song 269 | long duration = currentlyPlaying.durationMs; // Length of Song 270 | Serial.print("Elapsed time of song (ms): "); 271 | Serial.print(progress); 272 | Serial.print(" of "); 273 | Serial.println(duration); 274 | Serial.println(); 275 | 276 | float percentage = ((float)progress / (float)duration) * 100; 277 | int clampedPercentage = (int)percentage; 278 | Serial.print("<"); 279 | for (int j = 0; j < 50; j++) 280 | { 281 | if (clampedPercentage >= (j * 2)) 282 | { 283 | Serial.print("="); 284 | } 285 | else 286 | { 287 | Serial.print("-"); 288 | } 289 | } 290 | Serial.println(">"); 291 | Serial.println(); 292 | 293 | // will be in order of widest to narrowest 294 | // currentlyPlaying.numImages is the number of images that 295 | // are stored 296 | 297 | for (int i = 0; i < currentlyPlaying.numImages; i++) 298 | { 299 | // Save the second album image into the smallestImage Variable above. 300 | smallestImage = currentlyPlaying.albumImages[1]; 301 | Serial.println("------------------------"); 302 | Serial.print("Album Image: "); 303 | Serial.println(currentlyPlaying.albumImages[i].url); 304 | Serial.print("Dimensions: "); 305 | Serial.print(currentlyPlaying.albumImages[i].width); 306 | Serial.print(" x "); 307 | Serial.print(currentlyPlaying.albumImages[i].height); 308 | Serial.println(); 309 | } 310 | Serial.println("------------------------"); 311 | } 312 | 313 | void loop() 314 | { 315 | if (millis() > requestDueTime) 316 | { 317 | Serial.print("Free Heap: "); 318 | Serial.println(ESP.getFreeHeap()); 319 | 320 | Serial.println("getting currently playing song:"); 321 | // Check if music is playing currently on the account. 322 | int status = spotify.getCurrentlyPlaying(printCurrentlyPlayingToSerial, SPOTIFY_MARKET); 323 | if (status == 200) 324 | { 325 | Serial.println("Successfully got currently playing"); 326 | String newAlbum = String(smallestImage.url); 327 | if (newAlbum != lastAlbumArtUrl) 328 | { 329 | Serial.println("Updating Art"); 330 | char *my_url = const_cast(smallestImage.url); 331 | int displayImageResult = displayImage(my_url); 332 | 333 | if (displayImageResult == 0) 334 | { 335 | lastAlbumArtUrl = newAlbum; 336 | } 337 | else 338 | { 339 | Serial.print("failed to display image: "); 340 | Serial.println(displayImageResult); 341 | } 342 | } 343 | else if (status == 204) 344 | { 345 | Serial.println("Doesn't seem to be anything playing"); 346 | } 347 | else 348 | { 349 | Serial.print("Error: "); 350 | Serial.println(status); 351 | } 352 | 353 | requestDueTime = millis() + delayBetweenRequests; 354 | } 355 | } 356 | } 357 | 358 | // Method to draw messages at a certain point on a TFT Display. 359 | void drawMessage(int x, int y, char *message) 360 | { 361 | tft.setTextColor(TFT_WHITE); 362 | tft.drawString(message, x, y); 363 | } -------------------------------------------------------------------------------- /examples/getCurrentlyPlaying/getCurrentlyPlaying.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | Prints your currently playing track on spotify to the 3 | serial monitor using an ES32 or ESP8266 4 | 5 | NOTE: You need to get a Refresh token to use this example 6 | Use the getRefreshToken example to get it. 7 | 8 | Compatible Boards: 9 | - Any ESP8266 or ESP32 board 10 | 11 | Parts: 12 | ESP32 D1 Mini style Dev board* - http://s.click.aliexpress.com/e/C6ds4my 13 | 14 | * * = Affiliate 15 | 16 | If you find what I do useful and would like to support me, 17 | please consider becoming a sponsor on Github 18 | https://github.com/sponsors/witnessmenow/ 19 | 20 | 21 | Written by Brian Lough 22 | YouTube: https://www.youtube.com/brianlough 23 | Tindie: https://www.tindie.com/stores/brianlough/ 24 | Twitter: https://twitter.com/witnessmenow 25 | *******************************************************************/ 26 | 27 | // ---------------------------- 28 | // Standard Libraries 29 | // ---------------------------- 30 | 31 | #if defined(ESP8266) 32 | #include 33 | #elif defined(ESP32) 34 | #include 35 | #endif 36 | 37 | #include 38 | 39 | // ---------------------------- 40 | // Additional Libraries - each one of these will need to be installed. 41 | // ---------------------------- 42 | 43 | #include 44 | // Library for connecting to the Spotify API 45 | 46 | // Install from Github 47 | // https://github.com/witnessmenow/spotify-api-arduino 48 | 49 | // including a "spotify_server_cert" variable 50 | // header is included as part of the SpotifyArduino libary 51 | #include 52 | 53 | #include 54 | // Library used for parsing Json from the API responses 55 | 56 | // Search for "Arduino Json" in the Arduino Library manager 57 | // https://github.com/bblanchon/ArduinoJson 58 | 59 | //------- Replace the following! ------ 60 | 61 | char ssid[] = "SSID"; // your network SSID (name) 62 | char password[] = "password"; // your network password 63 | 64 | char clientId[] = "56t4373258u3405u43u543"; // Your client ID of your spotify APP 65 | char clientSecret[] = "56t4373258u3405u43u543"; // Your client Secret of your spotify APP (Do Not share this!) 66 | 67 | // Country code, including this is advisable 68 | #define SPOTIFY_MARKET "IE" 69 | 70 | #define SPOTIFY_REFRESH_TOKEN "AAAAAAAAAABBBBBBBBBBBCCCCCCCCCCCDDDDDDDDDDD" 71 | 72 | //------- ---------------------- ------ 73 | 74 | WiFiClientSecure client; 75 | SpotifyArduino spotify(client, clientId, clientSecret, SPOTIFY_REFRESH_TOKEN); 76 | 77 | unsigned long delayBetweenRequests = 60000; // Time between requests (1 minute) 78 | unsigned long requestDueTime; //time when request due 79 | 80 | void setup() 81 | { 82 | 83 | Serial.begin(115200); 84 | 85 | WiFi.mode(WIFI_STA); 86 | WiFi.begin(ssid, password); 87 | Serial.println(""); 88 | 89 | // Wait for connection 90 | while (WiFi.status() != WL_CONNECTED) 91 | { 92 | delay(500); 93 | Serial.print("."); 94 | } 95 | Serial.println(""); 96 | Serial.print("Connected to "); 97 | Serial.println(ssid); 98 | Serial.print("IP address: "); 99 | Serial.println(WiFi.localIP()); 100 | 101 | // Handle HTTPS Verification 102 | #if defined(ESP8266) 103 | client.setFingerprint(SPOTIFY_FINGERPRINT); // These expire every few months 104 | #elif defined(ESP32) 105 | client.setCACert(spotify_server_cert); 106 | #endif 107 | // ... or don't! 108 | //client.setInsecure(); 109 | 110 | // If you want to enable some extra debugging 111 | // uncomment the "#define SPOTIFY_DEBUG" in SpotifyArduino.h 112 | 113 | Serial.println("Refreshing Access Tokens"); 114 | if (!spotify.refreshAccessToken()) 115 | { 116 | Serial.println("Failed to get access tokens"); 117 | } 118 | } 119 | 120 | void printCurrentlyPlayingToSerial(CurrentlyPlaying currentlyPlaying) 121 | { 122 | // Use the details in this method or if you want to store them 123 | // make sure you copy them (using something like strncpy) 124 | // const char* artist = 125 | 126 | Serial.println("--------- Currently Playing ---------"); 127 | 128 | Serial.print("Is Playing: "); 129 | if (currentlyPlaying.isPlaying) 130 | { 131 | Serial.println("Yes"); 132 | } 133 | else 134 | { 135 | Serial.println("No"); 136 | } 137 | 138 | Serial.print("Track: "); 139 | Serial.println(currentlyPlaying.trackName); 140 | Serial.print("Track URI: "); 141 | Serial.println(currentlyPlaying.trackUri); 142 | Serial.println(); 143 | 144 | Serial.println("Artists: "); 145 | for (int i = 0; i < currentlyPlaying.numArtists; i++) 146 | { 147 | Serial.print("Name: "); 148 | Serial.println(currentlyPlaying.artists[i].artistName); 149 | Serial.print("Artist URI: "); 150 | Serial.println(currentlyPlaying.artists[i].artistUri); 151 | Serial.println(); 152 | } 153 | 154 | Serial.print("Album: "); 155 | Serial.println(currentlyPlaying.albumName); 156 | Serial.print("Album URI: "); 157 | Serial.println(currentlyPlaying.albumUri); 158 | Serial.println(); 159 | 160 | if (currentlyPlaying.contextUri != NULL) 161 | { 162 | Serial.print("Context URI: "); 163 | Serial.println(currentlyPlaying.contextUri); 164 | Serial.println(); 165 | } 166 | 167 | long progress = currentlyPlaying.progressMs; // duration passed in the song 168 | long duration = currentlyPlaying.durationMs; // Length of Song 169 | Serial.print("Elapsed time of song (ms): "); 170 | Serial.print(progress); 171 | Serial.print(" of "); 172 | Serial.println(duration); 173 | Serial.println(); 174 | 175 | float percentage = ((float)progress / (float)duration) * 100; 176 | int clampedPercentage = (int)percentage; 177 | Serial.print("<"); 178 | for (int j = 0; j < 50; j++) 179 | { 180 | if (clampedPercentage >= (j * 2)) 181 | { 182 | Serial.print("="); 183 | } 184 | else 185 | { 186 | Serial.print("-"); 187 | } 188 | } 189 | Serial.println(">"); 190 | Serial.println(); 191 | 192 | // will be in order of widest to narrowest 193 | // currentlyPlaying.numImages is the number of images that 194 | // are stored 195 | for (int i = 0; i < currentlyPlaying.numImages; i++) 196 | { 197 | Serial.println("------------------------"); 198 | Serial.print("Album Image: "); 199 | Serial.println(currentlyPlaying.albumImages[i].url); 200 | Serial.print("Dimensions: "); 201 | Serial.print(currentlyPlaying.albumImages[i].width); 202 | Serial.print(" x "); 203 | Serial.print(currentlyPlaying.albumImages[i].height); 204 | Serial.println(); 205 | } 206 | Serial.println("------------------------"); 207 | } 208 | 209 | void loop() 210 | { 211 | if (millis() > requestDueTime) 212 | { 213 | Serial.print("Free Heap: "); 214 | Serial.println(ESP.getFreeHeap()); 215 | 216 | Serial.println("getting currently playing song:"); 217 | // Market can be excluded if you want e.g. spotify.getCurrentlyPlaying() 218 | int status = spotify.getCurrentlyPlaying(printCurrentlyPlayingToSerial, SPOTIFY_MARKET); 219 | if (status == 200) 220 | { 221 | Serial.println("Successfully got currently playing"); 222 | } 223 | else if (status == 204) 224 | { 225 | Serial.println("Doesn't seem to be anything playing"); 226 | } 227 | else 228 | { 229 | Serial.print("Error: "); 230 | Serial.println(status); 231 | } 232 | requestDueTime = millis() + delayBetweenRequests; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /examples/getDevices/getDevices.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | Prints info about the devices currently connected to your spotify account 3 | and transfer playback between them. 4 | (useful for switching between your phone to your PC for example) 5 | 6 | NOTE: You need to get a Refresh token to use this example 7 | Use the getRefreshToken example to get it. 8 | 9 | Compatible Boards: 10 | - Any ESP8266 or ESP32 board 11 | 12 | Parts: 13 | ESP32 D1 Mini style Dev board* - http://s.click.aliexpress.com/e/C6ds4my 14 | 15 | * * = Affiliate 16 | 17 | If you find what I do useful and would like to support me, 18 | please consider becoming a sponsor on Github 19 | https://github.com/sponsors/witnessmenow/ 20 | 21 | 22 | Written by Brian Lough 23 | YouTube: https://www.youtube.com/brianlough 24 | Tindie: https://www.tindie.com/stores/brianlough/ 25 | Twitter: https://twitter.com/witnessmenow 26 | *******************************************************************/ 27 | 28 | // ---------------------------- 29 | // Standard Libraries 30 | // ---------------------------- 31 | 32 | #if defined(ESP8266) 33 | #include 34 | #elif defined(ESP32) 35 | #include 36 | #endif 37 | 38 | #include 39 | 40 | // ---------------------------- 41 | // Additional Libraries - each one of these will need to be installed. 42 | // ---------------------------- 43 | 44 | #include 45 | // Library for connecting to the Spotify API 46 | 47 | // Install from Github 48 | // https://github.com/witnessmenow/spotify-api-arduino 49 | 50 | // including a "spotify_server_cert" variable 51 | // header is included as part of the SpotifyArduino libary 52 | #include 53 | 54 | #include 55 | // Library used for parsing Json from the API responses 56 | 57 | // Search for "Arduino Json" in the Arduino Library manager 58 | // https://github.com/bblanchon/ArduinoJson 59 | 60 | //------- Replace the following! ------ 61 | 62 | char ssid[] = "SSID"; // your network SSID (name) 63 | char password[] = "password"; // your network password 64 | 65 | char clientId[] = "56t4373258u3405u43u543"; // Your client ID of your spotify APP 66 | char clientSecret[] = "56t4373258u3405u43u543"; // Your client Secret of your spotify APP (Do Not share this!) 67 | 68 | // Country code, including this is advisable 69 | #define SPOTIFY_MARKET "IE" 70 | 71 | #define SPOTIFY_REFRESH_TOKEN "AAAAAAAAAABBBBBBBBBBBCCCCCCCCCCCDDDDDDDDDDD" 72 | 73 | //------- ---------------------- ------ 74 | 75 | WiFiClientSecure client; 76 | SpotifyArduino spotify(client, clientId, clientSecret, SPOTIFY_REFRESH_TOKEN); 77 | 78 | unsigned long delayBetweenRequests = 60000; // Time between requests (1 minute) 79 | unsigned long requestDueTime; //time when request due 80 | 81 | // This is potentially optional depending on how you want to do it, 82 | // but we are going to store the important information the library returns here 83 | struct SimpleDevice 84 | { 85 | char name[SPOTIFY_DEVICE_NAME_CHAR_LENGTH]; 86 | char id[SPOTIFY_DEVICE_ID_CHAR_LENGTH]; 87 | }; 88 | 89 | #define MAX_DEVICES 6 90 | 91 | SimpleDevice deviceList[MAX_DEVICES]; 92 | int numberOfDevices = -1; 93 | 94 | void setup() 95 | { 96 | 97 | Serial.begin(115200); 98 | 99 | WiFi.mode(WIFI_STA); 100 | WiFi.begin(ssid, password); 101 | Serial.println(""); 102 | 103 | // Wait for connection 104 | while (WiFi.status() != WL_CONNECTED) 105 | { 106 | delay(500); 107 | Serial.print("."); 108 | } 109 | Serial.println(""); 110 | Serial.print("Connected to "); 111 | Serial.println(ssid); 112 | Serial.print("IP address: "); 113 | Serial.println(WiFi.localIP()); 114 | 115 | // Handle HTTPS Verification 116 | #if defined(ESP8266) 117 | client.setFingerprint(SPOTIFY_FINGERPRINT); // These expire every few months 118 | #elif defined(ESP32) 119 | client.setCACert(spotify_server_cert); 120 | #endif 121 | // ... or don't! 122 | //client.setInsecure(); 123 | 124 | // If you want to enable some extra debugging 125 | // uncomment the "#define SPOTIFY_DEBUG" in SpotifyArduino.h 126 | 127 | //spotify.getDevicesBufferSize = 3000; //can be adjusted, needs about 300 per device, defaults to 3000 128 | // Remember this is all devices returned by spotify, not just the max you want to return 129 | 130 | Serial.println("Refreshing Access Tokens"); 131 | if (!spotify.refreshAccessToken()) 132 | { 133 | Serial.println("Failed to get access tokens"); 134 | } 135 | } 136 | 137 | void printDeviceToSerial(SpotifyDevice device) 138 | { 139 | 140 | Serial.println("--------- Device Details ---------"); 141 | 142 | Serial.print("Device ID: "); 143 | Serial.println(device.id); 144 | 145 | Serial.print("Device Name: "); 146 | Serial.println(device.name); 147 | 148 | Serial.print("Device Type: "); 149 | Serial.println(device.type); 150 | 151 | Serial.print("Is Active: "); 152 | if (device.isActive) 153 | { 154 | Serial.println("Yes"); 155 | } 156 | else 157 | { 158 | Serial.println("No"); 159 | } 160 | 161 | Serial.print("Is Resticted: "); 162 | if (device.isRestricted) 163 | { 164 | Serial.println("Yes, from API docs \"no Web API commands will be accepted by this device\""); 165 | } 166 | else 167 | { 168 | Serial.println("No"); 169 | } 170 | 171 | Serial.print("Is Private Session: "); 172 | if (device.isPrivateSession) 173 | { 174 | Serial.println("Yes"); 175 | } 176 | else 177 | { 178 | Serial.println("No"); 179 | } 180 | 181 | Serial.print("Volume Percent: "); 182 | Serial.println(device.volumePercent); 183 | 184 | Serial.println("------------------------"); 185 | } 186 | 187 | bool getDeviceCallback(SpotifyDevice device, int index, int numDevices) 188 | { 189 | if (index == 0) 190 | { 191 | // This is a first device from this batch 192 | // lets set the number of devices we got back 193 | if (numDevices < MAX_DEVICES) 194 | { 195 | numberOfDevices = numDevices; 196 | } 197 | else 198 | { 199 | numberOfDevices = MAX_DEVICES; 200 | } 201 | } 202 | 203 | // We can't handle anymore than we can fit in our array 204 | if (index < MAX_DEVICES) 205 | { 206 | printDeviceToSerial(device); 207 | 208 | strncpy(deviceList[index].name, device.name, sizeof(deviceList[index].name)); //DO NOT use deviceList[index].name = device.name, it won't work as you expect! 209 | deviceList[index].name[sizeof(deviceList[index].name) - 1] = '\0'; //ensures its null terminated 210 | 211 | strncpy(deviceList[index].id, device.id, sizeof(deviceList[index].id)); 212 | deviceList[index].id[sizeof(deviceList[index].id) - 1] = '\0'; 213 | 214 | if (index == MAX_DEVICES - 1) 215 | { 216 | return false; //returning false stops it processing any more 217 | } 218 | else 219 | { 220 | return true; 221 | } 222 | } 223 | 224 | // We should never get here 225 | return false; //returning false stops it processing any more 226 | } 227 | 228 | void loop() 229 | { 230 | if (millis() > requestDueTime) 231 | { 232 | Serial.print("Free Heap: "); 233 | Serial.println(ESP.getFreeHeap()); 234 | 235 | Serial.println("Getting devices:"); 236 | int status = spotify.getDevices(getDeviceCallback); 237 | if (status == 200) 238 | { 239 | Serial.println("Successfully got devices, tranfering playback between them"); 240 | for (int i = 0; i < numberOfDevices; i++) 241 | { 242 | // You could do this transfer play back in the callback 243 | // But this example is more to simulate grabbing the devices 244 | // and having a UI to change between them 245 | spotify.transferPlayback(deviceList[i].id, true); //true means to play after transfer 246 | delay(5000); 247 | } 248 | } 249 | 250 | requestDueTime = millis() + delayBetweenRequests; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /examples/getRefreshToken/getRefreshToken.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | Get Refresh Token from spotify, this is needed for the other 3 | examples. 4 | 5 | Instructions: 6 | 7 | - Put in your Wifi details, Client ID, Client secret and flash to the board 8 | - Do one of the following 9 | 10 | --- IF USING IP (ESP32 MDNS does not work for me) 11 | 12 | - Get the Ip Address from the serial monitor 13 | - Add the following to Redirect URI on your Spotify app "http://[ESP_IP]/callback/" 14 | e.g. "http://192.168.1.20/callback/" (don't forget the last "/") 15 | - Open browser to esp using the IP 16 | 17 | --- IF USING MDNS 18 | 19 | - Search for "#define USE_IP_ADDRESS" and comment it out 20 | - Add the following to Redirect URI on your Spotify app "http://arduino.local/callback/" 21 | (don't forget the last "/") 22 | - Open browser to esp: http://arduino.local 23 | 24 | ----------- 25 | 26 | - Click the link on the webpage 27 | - The Refresh Token will be printed to screen, use this 28 | for SPOTIFY_REFRESH_TOKEN in other examples. 29 | 30 | Compatible Boards: 31 | - Any ESP8266 or ESP32 board 32 | 33 | Parts: 34 | ESP32 D1 Mini style Dev board* - http://s.click.aliexpress.com/e/C6ds4my 35 | 36 | * * = Affiliate 37 | 38 | If you find what I do useful and would like to support me, 39 | please consider becoming a sponsor on Github 40 | https://github.com/sponsors/witnessmenow/ 41 | 42 | 43 | Written by Brian Lough 44 | YouTube: https://www.youtube.com/brianlough 45 | Tindie: https://www.tindie.com/stores/brianlough/ 46 | Twitter: https://twitter.com/witnessmenow 47 | *******************************************************************/ 48 | 49 | // ---------------------------- 50 | // Standard Libraries 51 | // ---------------------------- 52 | 53 | #if defined(ESP8266) 54 | #include 55 | #include 56 | #include 57 | #elif defined(ESP32) 58 | #include 59 | #include 60 | #include 61 | #endif 62 | 63 | #include 64 | #include 65 | 66 | // ---------------------------- 67 | // Additional Libraries - each one of these will need to be installed. 68 | // ---------------------------- 69 | 70 | #include 71 | // Library for connecting to the Spotify API 72 | 73 | // Install from Github 74 | // https://github.com/witnessmenow/spotify-api-arduino 75 | 76 | // including a "spotify_server_cert" variable 77 | // header is included as part of the SpotifyArduino libary 78 | #include 79 | 80 | #include 81 | // Library used for parsing Json from the API responses 82 | 83 | // Search for "Arduino Json" in the Arduino Library manager 84 | // https://github.com/bblanchon/ArduinoJson 85 | 86 | //------- Replace the following! ------ 87 | 88 | char ssid[] = "SSID"; // your network SSID (name) 89 | char password[] = "password"; // your network password 90 | 91 | char clientId[] = "56t4373258u3405u43u543"; // Your client ID of your spotify APP 92 | char clientSecret[] = "56t4373258u3405u43u543"; // Your client Secret of your spotify APP (Do Not share this!) 93 | 94 | char scope[] = "user-read-playback-state%20user-modify-playback-state"; 95 | 96 | #define USE_IP_ADDRESS 1 //comment this out if you want to use MDNS 97 | 98 | #ifdef USE_IP_ADDRESS 99 | char callbackURItemplate[] = "%s%s%s"; 100 | char callbackURIProtocol[] = "http%3A%2F%2F"; // "http://" 101 | char callbackURIAddress[] = "%2Fcallback%2F"; // "/callback/" 102 | char callbackURI[100]; 103 | #else 104 | char callbackURI[] = "http%3A%2F%2Farduino.local%2Fcallback%2F"; 105 | #endif 106 | 107 | //------- ---------------------- ------ 108 | 109 | #if defined(ESP8266) 110 | ESP8266WebServer server(80); 111 | #elif defined(ESP32) 112 | WebServer server(80); 113 | #endif 114 | 115 | WiFiClientSecure client; 116 | SpotifyArduino spotify(client, clientId, clientSecret); 117 | 118 | const char *webpageTemplate = 119 | R"( 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
129 | spotify Auth 130 |
131 | 132 | 133 | )"; 134 | 135 | void handleRoot() 136 | { 137 | char webpage[800]; 138 | sprintf(webpage, webpageTemplate, clientId, callbackURI, scope); 139 | server.send(200, "text/html", webpage); 140 | } 141 | 142 | void handleCallback() 143 | { 144 | String code = ""; 145 | const char *refreshToken = NULL; 146 | for (uint8_t i = 0; i < server.args(); i++) 147 | { 148 | if (server.argName(i) == "code") 149 | { 150 | code = server.arg(i); 151 | refreshToken = spotify.requestAccessTokens(code.c_str(), callbackURI); 152 | } 153 | } 154 | 155 | if (refreshToken != NULL) 156 | { 157 | server.send(200, "text/plain", refreshToken); 158 | } 159 | else 160 | { 161 | server.send(404, "text/plain", "Failed to load token, check serial monitor"); 162 | } 163 | } 164 | 165 | void handleNotFound() 166 | { 167 | String message = "File Not Found\n\n"; 168 | message += "URI: "; 169 | message += server.uri(); 170 | message += "\nMethod: "; 171 | message += (server.method() == HTTP_GET) ? "GET" : "POST"; 172 | message += "\nArguments: "; 173 | message += server.args(); 174 | message += "\n"; 175 | 176 | for (uint8_t i = 0; i < server.args(); i++) 177 | { 178 | message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; 179 | } 180 | 181 | Serial.print(message); 182 | server.send(404, "text/plain", message); 183 | } 184 | 185 | void setup() 186 | { 187 | 188 | Serial.begin(115200); 189 | 190 | WiFi.mode(WIFI_STA); 191 | WiFi.begin(ssid, password); 192 | Serial.println(""); 193 | 194 | // Wait for connection 195 | while (WiFi.status() != WL_CONNECTED) 196 | { 197 | delay(500); 198 | Serial.print("."); 199 | } 200 | Serial.println(""); 201 | Serial.print("Connected to "); 202 | Serial.println(ssid); 203 | Serial.print("IP address: "); 204 | IPAddress ipAddress = WiFi.localIP(); 205 | Serial.println(ipAddress); 206 | 207 | if (MDNS.begin("arduino")) 208 | { 209 | Serial.println("MDNS responder started"); 210 | } 211 | 212 | // Handle HTTPS Verification 213 | #if defined(ESP8266) 214 | client.setFingerprint(SPOTIFY_FINGERPRINT); // These expire every few months 215 | #elif defined(ESP32) 216 | client.setCACert(spotify_server_cert); 217 | #endif 218 | // ... or don't! 219 | //client.setInsecure(); 220 | 221 | // If you want to enable some extra debugging 222 | // uncomment the "#define SPOTIFY_DEBUG" in SpotifyArduino.h 223 | 224 | #ifdef USE_IP_ADDRESS 225 | // Building up callback URL using IP address. 226 | sprintf(callbackURI, callbackURItemplate, callbackURIProtocol, ipAddress.toString().c_str(), callbackURIAddress); 227 | #endif 228 | 229 | server.on("/", handleRoot); 230 | server.on("/callback/", handleCallback); 231 | server.onNotFound(handleNotFound); 232 | server.begin(); 233 | Serial.println("HTTP server started"); 234 | } 235 | 236 | void loop() 237 | { 238 | #if defined(ESP8266) 239 | MDNS.update(); 240 | #endif 241 | 242 | server.handleClient(); 243 | } 244 | -------------------------------------------------------------------------------- /examples/getSearchResults/getSearchResults.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | Prints song information from a Spotify search query to the 3 | serial monitor using an ES32 or ESP8266 4 | 5 | NOTE: You need to get a Refresh token to use this example 6 | Use the getRefreshToken example to get it. 7 | 8 | Compatible Boards: 9 | - Any ESP8266 or ESP32 board 10 | 11 | Parts: 12 | ESP32 D1 Mini style Dev board* - http://s.click.aliexpress.com/e/C6ds4my 13 | 14 | * * = Affiliate 15 | 16 | If you find what I do useful and would like to support me, 17 | please consider becoming a sponsor on Github 18 | https://github.com/sponsors/witnessmenow/ 19 | 20 | 21 | Written by Brian Lough 22 | YouTube: https://www.youtube.com/brianlough 23 | Tindie: https://www.tindie.com/stores/brianlough/ 24 | Twitter: https://twitter.com/witnessmenow 25 | 26 | Search functionality by Jeremiah Ukwela 27 | *******************************************************************/ 28 | 29 | // ---------------------------- 30 | // Standard Libraries 31 | // ---------------------------- 32 | 33 | #if defined(ESP8266) 34 | #include 35 | #elif defined(ESP32) 36 | #include 37 | #endif 38 | 39 | #include 40 | 41 | // ---------------------------- 42 | // Additional Libraries - each one of these will need to be installed. 43 | // ---------------------------- 44 | 45 | #include 46 | // Library for connecting to the Spotify API 47 | 48 | // Install from Github 49 | // https://github.com/witnessmenow/spotify-api-arduino 50 | 51 | // including a "spotify_server_cert" variable 52 | // header is included as part of the SpotifyArduino libary 53 | #include 54 | 55 | #include 56 | // Library used for parsing Json from the API responses 57 | 58 | // Search for "Arduino Json" in the Arduino Library manager 59 | // https://github.com/bblanchon/ArduinoJson 60 | 61 | //------- Replace the following! ------ 62 | 63 | char ssid[] = "SSID"; // your network SSID (name) 64 | char password[] = "password"; // your network password 65 | 66 | char clientId[] = "56t4373258u3405u43u543"; // Your client ID of your spotify APP 67 | char clientSecret[] = "56t4373258u3405u43u543"; // Your client Secret of your spotify APP (Do Not share this!) 68 | 69 | // Country code, including this is advisable 70 | #define SPOTIFY_MARKET "IE" 71 | 72 | #define SPOTIFY_REFRESH_TOKEN "AAAAAAAAAABBBBBBBBBBBCCCCCCCCCCCDDDDDDDDDDD" 73 | 74 | //------- ---------------------- ------ 75 | 76 | WiFiClientSecure client; 77 | SpotifyArduino spotify(client, clientId, clientSecret, SPOTIFY_REFRESH_TOKEN); 78 | 79 | unsigned long delayBetweenRequests = 60000; // Time between requests (1 minute) 80 | unsigned long requestDueTime; //time when request due 81 | 82 | #define MAX_RESULTS 2 83 | 84 | void setup() 85 | { 86 | 87 | Serial.begin(115200); 88 | 89 | WiFi.mode(WIFI_STA); 90 | WiFi.begin(ssid, password); 91 | Serial.println(""); 92 | 93 | // Wait for connection 94 | while (WiFi.status() != WL_CONNECTED) 95 | { 96 | delay(500); 97 | Serial.print("."); 98 | } 99 | Serial.println(""); 100 | Serial.print("Connected to "); 101 | Serial.println(ssid); 102 | Serial.print("IP address: "); 103 | Serial.println(WiFi.localIP()); 104 | 105 | // Handle HTTPS Verification 106 | #if defined(ESP8266) 107 | client.setFingerprint(SPOTIFY_FINGERPRINT); // These expire every few months 108 | #elif defined(ESP32) 109 | client.setCACert(spotify_server_cert); 110 | #endif 111 | // ... or don't! 112 | //client.setInsecure(); 113 | 114 | // If you want to enable some extra debugging 115 | // uncomment the "#define SPOTIFY_DEBUG" in SpotifyArduino.h 116 | 117 | //spotify.searchDetailsBufferSize = 3000; //can be adjusted, needs about 500 per result, defaults to 3000 118 | 119 | Serial.println("Refreshing Access Tokens"); 120 | if (!spotify.refreshAccessToken()) 121 | { 122 | Serial.println("Failed to get access tokens"); 123 | } 124 | 125 | // If you get an out of memory error, you can increase the search buffer size, default is 3000 126 | // spotify.searchDetailsBufferSize = 3000; 127 | } 128 | 129 | bool getResultsCallback(SearchResult result, int index, int numResults) 130 | { 131 | 132 | Serial.println("--------- Song Details ---------"); 133 | 134 | Serial.print("Song Index: "); 135 | Serial.println(index); 136 | 137 | Serial.print("Song ID: "); 138 | Serial.println(result.trackUri); 139 | 140 | Serial.print("Song Name: "); 141 | Serial.println(result.trackName); 142 | 143 | Serial.print("Song Artists: "); 144 | for (int i = 0; i < result.numArtists; i++){ 145 | Serial.println(result.artists[i].artistName); 146 | } 147 | 148 | Serial.print("Song Album: "); 149 | Serial.println(result.albumName); 150 | 151 | Serial.print("Song Images (url): "); 152 | for (int i = 0; i < result.numImages; i++){ 153 | Serial.println(result.albumImages[i].url); 154 | } 155 | 156 | Serial.println("-------------------------------"); 157 | 158 | return true; 159 | } 160 | 161 | void loop() 162 | { 163 | if (millis() > requestDueTime) 164 | { 165 | Serial.print("Free Heap: "); 166 | Serial.println(ESP.getFreeHeap()); 167 | 168 | Serial.println("Making Search Request:"); 169 | 170 | String query = "/?q=artist:Toto&type=track&market=US&offset=1"; 171 | 172 | SearchResult results[MAX_RESULTS]; 173 | int status = spotify.searchForSong(query, MAX_RESULTS, getResultsCallback, results); 174 | //spotify.searchForSong(query, limit, callback, array); 175 | 176 | if (status == 200) 177 | { 178 | Serial.println("Successfully got results: printing information"); 179 | for (int i = 0; i < MAX_RESULTS; i++) 180 | { 181 | //Plays 30 seconds of each song from the search 182 | char body[100]; 183 | sprintf(body, "{\"uris\" : [\"%s\"]}", results[i].trackUri); 184 | if (spotify.playAdvanced(body)) 185 | { 186 | Serial.println("sent!"); 187 | } 188 | delay(30*1e3); 189 | } 190 | } 191 | 192 | requestDueTime = millis() + delayBetweenRequests; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /examples/playAdvanced/playAdvanced.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | Allow you to specify a track/artist/album to play. 3 | 4 | This is based on the request described here on the 5 | spotify documentation: 6 | https://developer.spotify.com/documentation/web-api/reference/player/start-a-users-playback/ 7 | 8 | This example shows some ways of invoking the above, but read the documentation 9 | for full details. 10 | 11 | NOTE: You need to get a Refresh token to use this example 12 | Use the getRefreshToken example to get it. 13 | 14 | Compatible Boards: 15 | - Any ESP8266 or ESP32 board 16 | 17 | Parts: 18 | ESP32 D1 Mini style Dev board* - http://s.click.aliexpress.com/e/C6ds4my 19 | 20 | * * = Affiliate 21 | 22 | If you find what I do useful and would like to support me, 23 | please consider becoming a sponsor on Github 24 | https://github.com/sponsors/witnessmenow/ 25 | 26 | 27 | Written by Brian Lough 28 | YouTube: https://www.youtube.com/brianlough 29 | Tindie: https://www.tindie.com/stores/brianlough/ 30 | Twitter: https://twitter.com/witnessmenow 31 | *******************************************************************/ 32 | 33 | // ---------------------------- 34 | // Standard Libraries 35 | // ---------------------------- 36 | 37 | #if defined(ESP8266) 38 | #include 39 | #elif defined(ESP32) 40 | #include 41 | #endif 42 | 43 | #include 44 | 45 | // ---------------------------- 46 | // Additional Libraries - each one of these will need to be installed. 47 | // ---------------------------- 48 | 49 | #include 50 | // Library for connecting to the Spotify API 51 | 52 | // Install from Github 53 | // https://github.com/witnessmenow/spotify-api-arduino 54 | 55 | // including a "spotify_server_cert" variable 56 | // header is included as part of the SpotifyArduino libary 57 | #include 58 | 59 | #include 60 | // Library used for parsing Json from the API responses 61 | 62 | // Search for "Arduino Json" in the Arduino Library manager 63 | // https://github.com/bblanchon/ArduinoJson 64 | 65 | //------- Replace the following! ------ 66 | 67 | char ssid[] = "SSID"; // your network SSID (name) 68 | char password[] = "password"; // your network password 69 | 70 | char clientId[] = "56t4373258u3405u43u543"; // Your client ID of your spotify APP 71 | char clientSecret[] = "56t4373258u3405u43u543"; // Your client Secret of your spotify APP (Do Not share this!) 72 | 73 | // Country code, including this is advisable 74 | #define SPOTIFY_MARKET "IE" 75 | 76 | #define SPOTIFY_REFRESH_TOKEN "AAAAAAAAAABBBBBBBBBBBCCCCCCCCCCCDDDDDDDDDDD" 77 | 78 | //------- ---------------------- ------ 79 | 80 | WiFiClientSecure client; 81 | SpotifyArduino spotify(client, clientId, clientSecret, SPOTIFY_REFRESH_TOKEN); 82 | 83 | void playSingleTrack() 84 | { 85 | char sampleTrack[] = "spotify:track:4uLU6hMCjMI75M1A2tKUQC"; 86 | char body[100]; 87 | sprintf(body, "{\"uris\" : [\"%s\"]}", sampleTrack); 88 | if (spotify.playAdvanced(body)) 89 | { 90 | Serial.println("sent!"); 91 | } 92 | } 93 | 94 | void playMultipleTracks() 95 | { 96 | char sampleTrack1[] = "spotify:track:6vW1WpedCmV4gtOijSoQV3"; 97 | char sampleTrack2[] = "spotify:track:4dJYjR2lM6SmYfLw2mnHvb"; 98 | char sampleTrack3[] = "spotify:track:4uLU6hMCjMI75M1A2tKUQC"; 99 | 100 | char body[200]; 101 | sprintf(body, "{\"uris\" : [\"%s\", \"%s\", \"%s\"]}", sampleTrack1, sampleTrack2, sampleTrack3); 102 | if (spotify.playAdvanced(body)) 103 | { 104 | Serial.println("sent!"); 105 | } 106 | } 107 | 108 | void playAlbum() 109 | { 110 | char sampleAlbum[] = "spotify:album:6N9PS4QXF1D0OWPk0Sxtb4"; 111 | 112 | char body[100]; 113 | sprintf(body, "{\"context_uri\" : \"%s\"}", sampleAlbum); 114 | if (spotify.playAdvanced(body)) 115 | { 116 | Serial.println("sent!"); 117 | } 118 | } 119 | 120 | void specifyTrackNumOfAlbum() 121 | { 122 | char sampleAlbum[] = "spotify:album:2fPcSpVFVo1dXEvarNoFkB"; 123 | // The position has an index of 0, so passing in 2 124 | // like this will actually play the 3rd song. 125 | int trackNum = 2; 126 | 127 | char body[200]; 128 | sprintf(body, "{\"context_uri\" : \"%s\", \"offset\": {\"position\": %d}}", sampleAlbum, trackNum); 129 | if (spotify.playAdvanced(body)) 130 | { 131 | Serial.println("sent!"); 132 | } 133 | } 134 | 135 | void specifyTrackOfAlbum() 136 | { 137 | char sampleAlbum[] = "spotify:album:2BLjT6yzDdKojUyc3Gi6y2"; 138 | char trackOnAlbum[] = "spotify:track:25IZtuJS77yXPCXMhPa1ze"; 139 | 140 | char body[200]; 141 | sprintf(body, "{\"context_uri\" : \"%s\", \"offset\": {\"uri\": \"%s\"}}", sampleAlbum, trackOnAlbum); 142 | if (spotify.playAdvanced(body)) 143 | { 144 | Serial.println("sent!"); 145 | } 146 | } 147 | 148 | void playArtist() 149 | { 150 | char sampleArtist[] = "spotify:artist:0gxyHStUsqpMadRV0Di1Qt"; 151 | 152 | char body[100]; 153 | sprintf(body, "{\"context_uri\" : \"%s\"}", sampleArtist); 154 | if (spotify.playAdvanced(body)) 155 | { 156 | Serial.println("sent!"); 157 | } 158 | } 159 | 160 | void playPlaylist() 161 | { 162 | char samplePlaylist[] = "spotify:playlist:37i9dQZF1DZ06evO05tE88"; 163 | 164 | char body[100]; 165 | sprintf(body, "{\"context_uri\" : \"%s\"}", samplePlaylist); 166 | if (spotify.playAdvanced(body)) 167 | { 168 | Serial.println("sent!"); 169 | } 170 | } 171 | 172 | void specifyTrackNumOfPlaylist() 173 | { 174 | char samplePlaylist[] = "spotify:playlist:37i9dQZF1DZ06evO05tE88"; 175 | // The position has an index of 0, so passing in 31 176 | // like this will actually play the 32nd song. 177 | int playlistTrackNum = 31; 178 | 179 | char body[200]; 180 | sprintf(body, "{\"context_uri\" : \"%s\", \"offset\": {\"position\": %d}}", samplePlaylist, playlistTrackNum); 181 | if (spotify.playAdvanced(body)) 182 | { 183 | Serial.println("sent!"); 184 | } 185 | } 186 | 187 | void specifyTrackOfPlaylist() 188 | { 189 | char samplePlaylist[] = "spotify:playlist:37i9dQZF1DZ06evO05tE88"; 190 | char trackOnPlaylist[] = "spotify:track:6vW1WpedCmV4gtOijSoQV3"; 191 | 192 | char body[200]; 193 | sprintf(body, "{\"context_uri\" : \"%s\", \"offset\": {\"uri\": \"%s\"}}", samplePlaylist, trackOnPlaylist); 194 | if (spotify.playAdvanced(body)) 195 | { 196 | Serial.println("sent!"); 197 | } 198 | } 199 | 200 | void setup() 201 | { 202 | 203 | Serial.begin(115200); 204 | 205 | // Set WiFi to station mode and disconnect from an AP if it was Previously 206 | // connected 207 | WiFi.mode(WIFI_STA); 208 | WiFi.disconnect(); 209 | delay(100); 210 | 211 | // Attempt to connect to Wifi network: 212 | Serial.print("Connecting Wifi: "); 213 | Serial.println(ssid); 214 | WiFi.begin(ssid, password); 215 | while (WiFi.status() != WL_CONNECTED) 216 | { 217 | Serial.print("."); 218 | delay(500); 219 | } 220 | Serial.println(""); 221 | Serial.println("WiFi connected"); 222 | Serial.println("IP address: "); 223 | IPAddress ip = WiFi.localIP(); 224 | Serial.println(ip); 225 | 226 | // Handle HTTPS Verification 227 | #if defined(ESP8266) 228 | client.setFingerprint(SPOTIFY_FINGERPRINT); // These expire every few months 229 | #elif defined(ESP32) 230 | client.setCACert(spotify_server_cert); 231 | #endif 232 | // ... or don't! 233 | //client.setInsecure(); 234 | 235 | // If you want to enable some extra debugging 236 | // uncomment the "#define SPOTIFY_DEBUG" in SpotifyArduino.h 237 | 238 | Serial.println("Refreshing Access Tokens"); 239 | if (!spotify.refreshAccessToken()) 240 | { 241 | Serial.println("Failed to get access tokens"); 242 | return; 243 | } 244 | 245 | delay(100); 246 | Serial.println("Playing Single Track"); 247 | playSingleTrack(); 248 | delay(10000); 249 | Serial.println("Playing Multiple Tracks"); 250 | playMultipleTracks(); 251 | delay(10000); 252 | Serial.println("Playing Album"); 253 | playAlbum(); 254 | delay(10000); 255 | Serial.println("Playing track number on Album"); 256 | specifyTrackNumOfAlbum(); 257 | delay(10000); 258 | Serial.println("Playing specific track on Album"); 259 | specifyTrackOfAlbum(); 260 | delay(10000); 261 | Serial.println("Playing Artist"); 262 | playArtist(); 263 | delay(10000); 264 | Serial.println("Playing Playlist"); 265 | playPlaylist(); 266 | delay(10000); 267 | Serial.println("Playing track number on Playlist"); 268 | specifyTrackNumOfPlaylist(); 269 | delay(10000); 270 | Serial.println("Playing specific track on Playlist"); 271 | specifyTrackOfPlaylist(); 272 | } 273 | 274 | // Example code is at end of setup 275 | void loop() 276 | { 277 | } 278 | -------------------------------------------------------------------------------- /examples/playerControls/playerControls.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | Controls spotify player using an ESP32 or ESP8266 3 | 4 | Supports: 5 | - Next Track 6 | - Previous Track 7 | - Seek 8 | 9 | NOTE: You need to get a Refresh token to use this example 10 | Use the getRefreshToken example to get it. 11 | 12 | Compatible Boards: 13 | - Any ESP8266 or ESP32 board 14 | 15 | Parts: 16 | ESP32 D1 Mini style Dev board* - http://s.click.aliexpress.com/e/C6ds4my 17 | 18 | * * = Affiliate 19 | 20 | If you find what I do useful and would like to support me, 21 | please consider becoming a sponsor on Github 22 | https://github.com/sponsors/witnessmenow/ 23 | 24 | 25 | Written by Brian Lough 26 | YouTube: https://www.youtube.com/brianlough 27 | Tindie: https://www.tindie.com/stores/brianlough/ 28 | Twitter: https://twitter.com/witnessmenow 29 | *******************************************************************/ 30 | 31 | // ---------------------------- 32 | // Standard Libraries 33 | // ---------------------------- 34 | 35 | #if defined(ESP8266) 36 | #include 37 | #elif defined(ESP32) 38 | #include 39 | #endif 40 | 41 | #include 42 | 43 | // ---------------------------- 44 | // Additional Libraries - each one of these will need to be installed. 45 | // ---------------------------- 46 | 47 | #include 48 | // Library for connecting to the Spotify API 49 | 50 | // Install from Github 51 | // https://github.com/witnessmenow/spotify-api-arduino 52 | 53 | // including a "spotify_server_cert" variable 54 | // header is included as part of the SpotifyArduino libary 55 | #include 56 | 57 | #include 58 | // Library used for parsing Json from the API responses 59 | 60 | // Search for "Arduino Json" in the Arduino Library manager 61 | // https://github.com/bblanchon/ArduinoJson 62 | 63 | //------- Replace the following! ------ 64 | 65 | char ssid[] = "SSID"; // your network SSID (name) 66 | char password[] = "password"; // your network password 67 | 68 | char clientId[] = "56t4373258u3405u43u543"; // Your client ID of your spotify APP 69 | char clientSecret[] = "56t4373258u3405u43u543"; // Your client Secret of your spotify APP (Do Not share this!) 70 | 71 | // Country code, including this is advisable 72 | #define SPOTIFY_MARKET "IE" 73 | 74 | #define SPOTIFY_REFRESH_TOKEN "AAAAAAAAAABBBBBBBBBBBCCCCCCCCCCCDDDDDDDDDDD" 75 | 76 | //------- ---------------------- ------ 77 | 78 | WiFiClientSecure client; 79 | SpotifyArduino spotify(client, clientId, clientSecret, SPOTIFY_REFRESH_TOKEN); 80 | 81 | void setup() 82 | { 83 | 84 | Serial.begin(115200); 85 | 86 | WiFi.mode(WIFI_STA); 87 | WiFi.begin(ssid, password); 88 | Serial.println(""); 89 | 90 | // Wait for connection 91 | while (WiFi.status() != WL_CONNECTED) 92 | { 93 | delay(500); 94 | Serial.print("."); 95 | } 96 | Serial.println(""); 97 | Serial.print("Connected to "); 98 | Serial.println(ssid); 99 | Serial.print("IP address: "); 100 | Serial.println(WiFi.localIP()); 101 | 102 | // Handle HTTPS Verification 103 | #if defined(ESP8266) 104 | client.setFingerprint(SPOTIFY_FINGERPRINT); // These expire every few months 105 | #elif defined(ESP32) 106 | client.setCACert(spotify_server_cert); 107 | #endif 108 | // ... or don't! 109 | //client.setInsecure(); 110 | 111 | // If you want to enable some extra debugging 112 | // uncomment the "#define SPOTIFY_DEBUG" in SpotifyArduino.h 113 | 114 | Serial.println("Refreshing Access Tokens"); 115 | if (!spotify.refreshAccessToken()) 116 | { 117 | Serial.println("Failed to get access tokens"); 118 | } 119 | 120 | delay(1000); 121 | Serial.print("Going to start of track..."); 122 | if (spotify.seek(0)) 123 | { 124 | Serial.println("done!"); 125 | } 126 | delay(2000); 127 | Serial.print("Going to previous track..."); 128 | if (spotify.previousTrack()) 129 | { 130 | Serial.println("done!"); 131 | } 132 | delay(2000); 133 | Serial.print("Skipping to next track..."); 134 | if (spotify.nextTrack()) 135 | { 136 | Serial.println("done!"); 137 | } 138 | 139 | // Setting volume doesn't seem to work on my Android Phone 140 | // It does work on my Desktop client 141 | delay(2000); 142 | Serial.print("set Volume 10%..."); 143 | if (spotify.setVolume(10)) 144 | { 145 | Serial.println("done!"); 146 | } 147 | delay(2000); 148 | Serial.print("set Volume 70%..."); 149 | if (spotify.setVolume(70)) 150 | { 151 | Serial.println("done!"); 152 | } 153 | delay(2000); 154 | Serial.print("Pausing..."); 155 | if (spotify.pause()) 156 | { 157 | Serial.println("done!"); 158 | } 159 | delay(2000); 160 | Serial.print("Playing..."); 161 | if (spotify.play()) 162 | { 163 | Serial.println("done!"); 164 | } 165 | 166 | delay(3000); 167 | Serial.print("enabling shuffle..."); 168 | if (spotify.toggleShuffle(true)) 169 | { 170 | Serial.println("done!"); 171 | } 172 | delay(3000); 173 | Serial.print("disabling shuffle..."); 174 | if (spotify.toggleShuffle(false)) 175 | { 176 | Serial.println("done!"); 177 | } 178 | 179 | delay(3000); 180 | Serial.print("Setting repeat mode to 'track'..."); 181 | if (spotify.setRepeatMode(repeat_track)) 182 | { 183 | Serial.println("done!"); 184 | } 185 | delay(3000); 186 | Serial.print("Setting repeat mode to 'context'..."); 187 | if (spotify.setRepeatMode(repeat_context)) 188 | { 189 | Serial.println("done!"); 190 | } 191 | delay(3000); 192 | Serial.print("Setting repeat mode to 'off'..."); 193 | if (spotify.setRepeatMode(repeat_off)) 194 | { 195 | Serial.println("done!"); 196 | } 197 | } 198 | 199 | // Example code is at end of setup 200 | void loop() 201 | { 202 | } 203 | -------------------------------------------------------------------------------- /examples/playerDetails/playerDetails.ino: -------------------------------------------------------------------------------- 1 | /******************************************************************* 2 | Prints info about your currently active spotify device 3 | on the serial monitor using an ES32 or ESP8266 4 | 5 | NOTE: You need to get a Refresh token to use this example 6 | Use the getRefreshToken example to get it. 7 | 8 | Compatible Boards: 9 | - Any ESP8266 or ESP32 board 10 | 11 | Parts: 12 | ESP32 D1 Mini style Dev board* - http://s.click.aliexpress.com/e/C6ds4my 13 | 14 | * * = Affiliate 15 | 16 | If you find what I do useful and would like to support me, 17 | please consider becoming a sponsor on Github 18 | https://github.com/sponsors/witnessmenow/ 19 | 20 | 21 | Written by Brian Lough 22 | YouTube: https://www.youtube.com/brianlough 23 | Tindie: https://www.tindie.com/stores/brianlough/ 24 | Twitter: https://twitter.com/witnessmenow 25 | *******************************************************************/ 26 | 27 | // ---------------------------- 28 | // Standard Libraries 29 | // ---------------------------- 30 | 31 | #if defined(ESP8266) 32 | #include 33 | #elif defined(ESP32) 34 | #include 35 | #endif 36 | 37 | #include 38 | 39 | // ---------------------------- 40 | // Additional Libraries - each one of these will need to be installed. 41 | // ---------------------------- 42 | 43 | #include 44 | // Library for connecting to the Spotify API 45 | 46 | // Install from Github 47 | // https://github.com/witnessmenow/spotify-api-arduino 48 | 49 | // including a "spotify_server_cert" variable 50 | // header is included as part of the SpotifyArduino libary 51 | #include 52 | 53 | #include 54 | // Library used for parsing Json from the API responses 55 | 56 | // Search for "Arduino Json" in the Arduino Library manager 57 | // https://github.com/bblanchon/ArduinoJson 58 | 59 | //------- Replace the following! ------ 60 | 61 | char ssid[] = "SSID"; // your network SSID (name) 62 | char password[] = "password"; // your network password 63 | 64 | char clientId[] = "56t4373258u3405u43u543"; // Your client ID of your spotify APP 65 | char clientSecret[] = "56t4373258u3405u43u543"; // Your client Secret of your spotify APP (Do Not share this!) 66 | 67 | // Country code, including this is advisable 68 | #define SPOTIFY_MARKET "IE" 69 | 70 | #define SPOTIFY_REFRESH_TOKEN "AAAAAAAAAABBBBBBBBBBBCCCCCCCCCCCDDDDDDDDDDD" 71 | 72 | //------- ---------------------- ------ 73 | 74 | WiFiClientSecure client; 75 | SpotifyArduino spotify(client, clientId, clientSecret, SPOTIFY_REFRESH_TOKEN); 76 | 77 | unsigned long delayBetweenRequests = 60000; // Time between requests (1 minute) 78 | unsigned long requestDueTime; //time when request due 79 | 80 | void setup() 81 | { 82 | 83 | Serial.begin(115200); 84 | 85 | WiFi.mode(WIFI_STA); 86 | WiFi.begin(ssid, password); 87 | Serial.println(""); 88 | 89 | // Wait for connection 90 | while (WiFi.status() != WL_CONNECTED) 91 | { 92 | delay(500); 93 | Serial.print("."); 94 | } 95 | Serial.println(""); 96 | Serial.print("Connected to "); 97 | Serial.println(ssid); 98 | Serial.print("IP address: "); 99 | Serial.println(WiFi.localIP()); 100 | 101 | // Handle HTTPS Verification 102 | #if defined(ESP8266) 103 | client.setFingerprint(SPOTIFY_FINGERPRINT); // These expire every few months 104 | #elif defined(ESP32) 105 | client.setCACert(spotify_server_cert); 106 | #endif 107 | // ... or don't! 108 | //client.setInsecure(); 109 | 110 | // If you want to enable some extra debugging 111 | // uncomment the "#define SPOTIFY_DEBUG" in SpotifyArduino.h 112 | 113 | Serial.println("Refreshing Access Tokens"); 114 | if (!spotify.refreshAccessToken()) 115 | { 116 | Serial.println("Failed to get access tokens"); 117 | } 118 | } 119 | 120 | void printPlayerDetailsToSerial(PlayerDetails playerDetails) 121 | { 122 | Serial.println("--------- Player Details ---------"); 123 | 124 | Serial.print("Device ID: "); 125 | Serial.println(playerDetails.device.id); 126 | 127 | Serial.print("Device Name: "); 128 | Serial.println(playerDetails.device.name); 129 | 130 | Serial.print("Device Type: "); 131 | Serial.println(playerDetails.device.type); 132 | 133 | Serial.print("Is Active: "); 134 | if (playerDetails.device.isActive) 135 | { 136 | Serial.println("Yes"); 137 | } 138 | else 139 | { 140 | Serial.println("No"); 141 | } 142 | 143 | Serial.print("Is Resticted: "); 144 | if (playerDetails.device.isRestricted) 145 | { 146 | Serial.println("Yes, from API docs \"no Web API commands will be accepted by this device\""); 147 | } 148 | else 149 | { 150 | Serial.println("No"); 151 | } 152 | 153 | Serial.print("Is Private Session: "); 154 | if (playerDetails.device.isPrivateSession) 155 | { 156 | Serial.println("Yes"); 157 | } 158 | else 159 | { 160 | Serial.println("No"); 161 | } 162 | 163 | Serial.print("Volume Percent: "); 164 | Serial.println(playerDetails.device.volumePercent); 165 | 166 | Serial.print("Progress (Ms): "); 167 | Serial.println(playerDetails.progressMs); 168 | 169 | Serial.print("Is Playing: "); 170 | if (playerDetails.isPlaying) 171 | { 172 | Serial.println("Yes"); 173 | } 174 | else 175 | { 176 | Serial.println("No"); 177 | } 178 | 179 | Serial.print("Shuffle State: "); 180 | if (playerDetails.shuffleState) 181 | { 182 | Serial.println("On"); 183 | } 184 | else 185 | { 186 | Serial.println("Off"); 187 | } 188 | 189 | Serial.print("Repeat State: "); 190 | switch (playerDetails.repeateState) 191 | { 192 | case repeat_track: 193 | Serial.println("track"); 194 | break; 195 | case repeat_context: 196 | Serial.println("context"); 197 | break; 198 | case repeat_off: 199 | Serial.println("off"); 200 | break; 201 | } 202 | 203 | Serial.println("------------------------"); 204 | } 205 | 206 | void loop() 207 | { 208 | if (millis() > requestDueTime) 209 | { 210 | Serial.print("Free Heap: "); 211 | Serial.println(ESP.getFreeHeap()); 212 | 213 | Serial.println("Getting player Details:"); 214 | // Market can be excluded if you want e.g. spotify.getPlayerDetails() 215 | int status = spotify.getPlayerDetails(printPlayerDetailsToSerial, SPOTIFY_MARKET); 216 | if (status == 200) 217 | { 218 | Serial.println("Successfully got player details"); 219 | } 220 | else if (status == 204) 221 | { 222 | Serial.println("No active player?"); 223 | } 224 | else 225 | { 226 | Serial.print("Error: "); 227 | Serial.println(status); 228 | } 229 | 230 | requestDueTime = millis() + delayBetweenRequests; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"SpotifyArduino", 3 | "description":"A library to wrap the Spotify API (supports ESP8266/ESP32 & probably others)", 4 | "keywords":"Spotify,Music,Arduino", 5 | "authors": 6 | { 7 | "name": "Brian Lough ", 8 | "maintainer": true 9 | }, 10 | "repository": 11 | { 12 | "type": "git", 13 | "url": "https://github.com/witnessmenow/spotify-api-arduino" 14 | }, 15 | "version": "0.1.0", 16 | "license": "MIT", 17 | "frameworks": "arduino", 18 | "platforms": "*", 19 | "dependencies": [ 20 | { 21 | "name": "bblanchon/ArduinoJson" 22 | } 23 | ], 24 | "build": { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=SpotifyArduino 2 | version=0.1.0 3 | author=Brian Lough 4 | maintainer=Brian Lough 5 | sentence=A library to wrap the Spotify API (supports ESP8266/ESP32 & probably others) 6 | paragraph=A library to wrap the Spotify API. This does not play music! It can be used to see what is currently playing and control the player. 7 | category=Communication 8 | url=https://github.com/witnessmenow/spotify-api-arduino 9 | architectures=* -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | 3 | ; 64 ArduinoJson 4 | ; by Benoit Blanchon 5 | ; Repository: https://github.com/bblanchon/ArduinoJson.git 6 | ; 5538 WiFiNINA 7 | ; by Arduino 8 | ; Repository: https://github.com/arduino-libraries/WiFiNINA.git 9 | ; 3577 ESP32 64x32 LED MATRIX HUB75 DMA Display 10 | ; by Faptastic 11 | ; Repository: https://github.com/mrfaptastic/ESP32-RGB64x32MatrixPanel-I2S-DMA.git 12 | ; 13 Adafruit GFX Library 13 | ; by Adafruit 14 | ; Repository: https://github.com/adafruit/Adafruit-GFX-Library.git 15 | ; 6906 TJpg_Decoder 16 | ; by Bodmer 17 | ; Repository: https://github.com/Bodmer/TJpg_Decoder.git 18 | ; 6214 Adafruit BusIO 19 | ; by Adafruit 20 | ; Repository: https://github.com/adafruit/Adafruit_BusIO.git 21 | 22 | [platformio] 23 | description = Arduino Spotify API Library 24 | default_envs = d1_mini 25 | 26 | [env] 27 | lib_deps = 28 | ArduinoJson 29 | 3577 30 | 13 31 | 6906 32 | 6214 33 | Wire 34 | monitor_speed = 115200 35 | 36 | [env:d1_mini] 37 | platform = espressif8266 38 | board = d1_mini 39 | framework = arduino 40 | 41 | [env:esp32dev] 42 | platform = espressif32 43 | board = esp32dev 44 | framework = arduino 45 | 46 | [env:nano_33_iot] 47 | platform = atmelsam 48 | board = nano_33_iot 49 | framework = arduino -------------------------------------------------------------------------------- /scripts/travis/platformio.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eux 2 | 3 | platformio ci $PWD/examples/$BOARDTYPE$EXAMPLE_FOLDER$EXAMPLE_NAME/ -b $BOARD --lib="." 4 | -------------------------------------------------------------------------------- /scripts/travis/platformioSingle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -eux 2 | 3 | platformio ci $PWD/examples/$EXAMPLE_FOLDER$EXAMPLE_NAME/$EXAMPLE_NAME.ino -l '.' -b $BOARD 4 | -------------------------------------------------------------------------------- /src/SpotifyArduino.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SpotifyArduino - An Arduino library to wrap the Spotify API 3 | 4 | Copyright (c) 2021 Brian Lough. 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this library; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | 21 | #include "SpotifyArduino.h" 22 | 23 | SpotifyArduino::SpotifyArduino(Client &client) 24 | { 25 | this->client = &client; 26 | } 27 | 28 | SpotifyArduino::SpotifyArduino(Client &client, char *bearerToken) 29 | { 30 | this->client = &client; 31 | sprintf(this->_bearerToken, "Bearer %s", bearerToken); 32 | } 33 | 34 | SpotifyArduino::SpotifyArduino(Client &client, const char *clientId, const char *clientSecret, const char *refreshToken) 35 | { 36 | this->client = &client; 37 | this->_clientId = clientId; 38 | this->_clientSecret = clientSecret; 39 | setRefreshToken(refreshToken); 40 | } 41 | 42 | int SpotifyArduino::makeRequestWithBody(const char *type, const char *command, const char *authorization, const char *body, const char *contentType, const char *host) 43 | { 44 | client->flush(); 45 | #ifdef SPOTIFY_DEBUG 46 | Serial.println(host); 47 | #endif 48 | client->setTimeout(SPOTIFY_TIMEOUT); 49 | if (!client->connect(host, portNumber)) 50 | { 51 | #ifdef SPOTIFY_SERIAL_OUTPUT 52 | Serial.println(F("Connection failed")); 53 | #endif 54 | return -1; 55 | } 56 | 57 | // give the esp a breather 58 | yield(); 59 | 60 | // Send HTTP request 61 | client->print(type); 62 | client->print(command); 63 | client->println(F(" HTTP/1.0")); 64 | 65 | //Headers 66 | client->print(F("Host: ")); 67 | client->println(host); 68 | 69 | client->println(F("Accept: application/json")); 70 | client->print(F("Content-Type: ")); 71 | client->println(contentType); 72 | 73 | if (authorization != NULL) 74 | { 75 | client->print(F("Authorization: ")); 76 | client->println(authorization); 77 | } 78 | 79 | client->println(F("Cache-Control: no-cache")); 80 | 81 | client->print(F("Content-Length: ")); 82 | client->println(strlen(body)); 83 | 84 | client->println(); 85 | 86 | client->print(body); 87 | 88 | if (client->println() == 0) 89 | { 90 | #ifdef SPOTIFY_SERIAL_OUTPUT 91 | Serial.println(F("Failed to send request")); 92 | #endif 93 | return -2; 94 | } 95 | 96 | int statusCode = getHttpStatusCode(); 97 | return statusCode; 98 | } 99 | 100 | int SpotifyArduino::makePutRequest(const char *command, const char *authorization, const char *body, const char *contentType, const char *host) 101 | { 102 | return makeRequestWithBody("PUT ", command, authorization, body, contentType); 103 | } 104 | 105 | int SpotifyArduino::makePostRequest(const char *command, const char *authorization, const char *body, const char *contentType, const char *host) 106 | { 107 | return makeRequestWithBody("POST ", command, authorization, body, contentType, host); 108 | } 109 | 110 | int SpotifyArduino::makeGetRequest(const char *command, const char *authorization, const char *accept, const char *host) 111 | { 112 | client->flush(); 113 | client->setTimeout(SPOTIFY_TIMEOUT); 114 | if (!client->connect(host, portNumber)) 115 | { 116 | #ifdef SPOTIFY_SERIAL_OUTPUT 117 | Serial.println(F("Connection failed")); 118 | #endif 119 | return -1; 120 | } 121 | 122 | // give the esp a breather 123 | yield(); 124 | 125 | // Send HTTP request 126 | client->print(F("GET ")); 127 | client->print(command); 128 | client->println(F(" HTTP/1.0")); 129 | 130 | //Headers 131 | client->print(F("Host: ")); 132 | client->println(host); 133 | 134 | if (accept != NULL) 135 | { 136 | client->print(F("Accept: ")); 137 | client->println(accept); 138 | } 139 | 140 | if (authorization != NULL) 141 | { 142 | client->print(F("Authorization: ")); 143 | client->println(authorization); 144 | } 145 | 146 | client->println(F("Cache-Control: no-cache")); 147 | 148 | if (client->println() == 0) 149 | { 150 | #ifdef SPOTIFY_SERIAL_OUTPUT 151 | Serial.println(F("Failed to send request")); 152 | #endif 153 | return -2; 154 | } 155 | 156 | int statusCode = getHttpStatusCode(); 157 | 158 | return statusCode; 159 | } 160 | 161 | void SpotifyArduino::setRefreshToken(const char *refreshToken) 162 | { 163 | int newRefreshTokenLen = strlen(refreshToken); 164 | if (_refreshToken == NULL || strlen(_refreshToken) < newRefreshTokenLen) 165 | { 166 | delete _refreshToken; 167 | _refreshToken = new char[newRefreshTokenLen + 1](); 168 | } 169 | 170 | strncpy(_refreshToken, refreshToken, newRefreshTokenLen + 1); 171 | } 172 | 173 | bool SpotifyArduino::refreshAccessToken() 174 | { 175 | char body[300]; 176 | sprintf(body, refreshAccessTokensBody, _refreshToken, _clientId, _clientSecret); 177 | 178 | #ifdef SPOTIFY_DEBUG 179 | Serial.println(body); 180 | printStack(); 181 | #endif 182 | 183 | int statusCode = makePostRequest(SPOTIFY_TOKEN_ENDPOINT, NULL, body, "application/x-www-form-urlencoded", SPOTIFY_ACCOUNTS_HOST); 184 | if (statusCode > 0) 185 | { 186 | skipHeaders(); 187 | } 188 | unsigned long now = millis(); 189 | 190 | #ifdef SPOTIFY_DEBUG 191 | Serial.print("status Code"); 192 | Serial.println(statusCode); 193 | #endif 194 | 195 | bool refreshed = false; 196 | if (statusCode == 200) 197 | { 198 | StaticJsonDocument<48> filter; 199 | filter["access_token"] = true; 200 | filter["token_type"] = true; 201 | filter["expires_in"] = true; 202 | 203 | DynamicJsonDocument doc(512); 204 | 205 | // Parse JSON object 206 | #ifndef SPOTIFY_PRINT_JSON_PARSE 207 | DeserializationError error = deserializeJson(doc, *client, DeserializationOption::Filter(filter)); 208 | #else 209 | ReadLoggingStream loggingStream(*client, Serial); 210 | DeserializationError error = deserializeJson(doc, loggingStream, DeserializationOption::Filter(filter)); 211 | #endif 212 | if (!error) 213 | { 214 | #ifdef SPOTIFY_DEBUG 215 | Serial.println(F("No JSON error, dealing with response")); 216 | #endif 217 | const char *accessToken = doc["access_token"].as(); 218 | if (accessToken != NULL && (SPOTIFY_ACCESS_TOKEN_LENGTH >= strlen(accessToken))) 219 | { 220 | sprintf(this->_bearerToken, "Bearer %s", accessToken); 221 | int tokenTtl = doc["expires_in"]; // Usually 3600 (1 hour) 222 | tokenTimeToLiveMs = (tokenTtl * 1000) - 2000; // The 2000 is just to force the token expiry to check if its very close 223 | timeTokenRefreshed = now; 224 | refreshed = true; 225 | } 226 | else 227 | { 228 | #ifdef SPOTIFY_SERIAL_OUTPUT 229 | Serial.print(F("Problem with access_token (too long or null): ")); 230 | Serial.println(accessToken); 231 | #endif 232 | } 233 | } 234 | else 235 | { 236 | #ifdef SPOTIFY_SERIAL_OUTPUT 237 | Serial.print(F("deserializeJson() failed with code ")); 238 | Serial.println(error.c_str()); 239 | #endif 240 | } 241 | } 242 | else 243 | { 244 | parseError(); 245 | } 246 | 247 | closeClient(); 248 | return refreshed; 249 | } 250 | 251 | bool SpotifyArduino::checkAndRefreshAccessToken() 252 | { 253 | unsigned long timeSinceLastRefresh = millis() - timeTokenRefreshed; 254 | if (timeSinceLastRefresh >= tokenTimeToLiveMs) 255 | { 256 | #ifdef SPOTIFY_SERIAL_OUTPUT 257 | Serial.println("Refresh of the Access token is due, doing that now."); 258 | #endif 259 | return refreshAccessToken(); 260 | } 261 | 262 | // Token is still valid 263 | return true; 264 | } 265 | 266 | const char *SpotifyArduino::requestAccessTokens(const char *code, const char *redirectUrl) 267 | { 268 | 269 | char body[500]; 270 | sprintf(body, requestAccessTokensBody, code, redirectUrl, _clientId, _clientSecret); 271 | 272 | #ifdef SPOTIFY_DEBUG 273 | Serial.println(body); 274 | #endif 275 | 276 | int statusCode = makePostRequest(SPOTIFY_TOKEN_ENDPOINT, NULL, body, "application/x-www-form-urlencoded", SPOTIFY_ACCOUNTS_HOST); 277 | if (statusCode > 0) 278 | { 279 | skipHeaders(); 280 | } 281 | unsigned long now = millis(); 282 | 283 | #ifdef SPOTIFY_DEBUG 284 | Serial.print("status Code"); 285 | Serial.println(statusCode); 286 | #endif 287 | 288 | if (statusCode == 200) 289 | { 290 | DynamicJsonDocument doc(1000); 291 | // Parse JSON object 292 | #ifndef SPOTIFY_PRINT_JSON_PARSE 293 | DeserializationError error = deserializeJson(doc, *client); 294 | #else 295 | ReadLoggingStream loggingStream(*client, Serial); 296 | DeserializationError error = deserializeJson(doc, loggingStream); 297 | #endif 298 | if (!error) 299 | { 300 | sprintf(this->_bearerToken, "Bearer %s", doc["access_token"].as()); 301 | setRefreshToken(doc["refresh_token"].as()); 302 | int tokenTtl = doc["expires_in"]; // Usually 3600 (1 hour) 303 | tokenTimeToLiveMs = (tokenTtl * 1000) - 2000; // The 2000 is just to force the token expiry to check if its very close 304 | timeTokenRefreshed = now; 305 | } 306 | else 307 | { 308 | #ifdef SPOTIFY_SERIAL_OUTPUT 309 | Serial.print(F("deserializeJson() failed with code ")); 310 | Serial.println(error.c_str()); 311 | #endif 312 | } 313 | } 314 | else 315 | { 316 | parseError(); 317 | } 318 | 319 | closeClient(); 320 | return _refreshToken; 321 | } 322 | 323 | bool SpotifyArduino::play(const char *deviceId) 324 | { 325 | char command[100] = SPOTIFY_PLAY_ENDPOINT; 326 | return playerControl(command, deviceId); 327 | } 328 | 329 | bool SpotifyArduino::playAdvanced(char *body, const char *deviceId) 330 | { 331 | char command[100] = SPOTIFY_PLAY_ENDPOINT; 332 | return playerControl(command, deviceId, body); 333 | } 334 | 335 | bool SpotifyArduino::pause(const char *deviceId) 336 | { 337 | char command[100] = SPOTIFY_PAUSE_ENDPOINT; 338 | return playerControl(command, deviceId); 339 | } 340 | 341 | bool SpotifyArduino::setVolume(int volume, const char *deviceId) 342 | { 343 | char command[125]; 344 | sprintf(command, SPOTIFY_VOLUME_ENDPOINT, volume); 345 | return playerControl(command, deviceId); 346 | } 347 | 348 | bool SpotifyArduino::toggleShuffle(bool shuffle, const char *deviceId) 349 | { 350 | char command[125]; 351 | char shuffleState[10]; 352 | if (shuffle) 353 | { 354 | strcpy(shuffleState, "true"); 355 | } 356 | else 357 | { 358 | strcpy(shuffleState, "false"); 359 | } 360 | sprintf(command, SPOTIFY_SHUFFLE_ENDPOINT, shuffleState); 361 | return playerControl(command, deviceId); 362 | } 363 | 364 | bool SpotifyArduino::setRepeatMode(RepeatOptions repeat, const char *deviceId) 365 | { 366 | char command[125]; 367 | char repeatState[10]; 368 | switch (repeat) 369 | { 370 | case repeat_track: 371 | strcpy(repeatState, "track"); 372 | break; 373 | case repeat_context: 374 | strcpy(repeatState, "context"); 375 | break; 376 | case repeat_off: 377 | strcpy(repeatState, "off"); 378 | break; 379 | } 380 | 381 | sprintf(command, SPOTIFY_REPEAT_ENDPOINT, repeatState); 382 | return playerControl(command, deviceId); 383 | } 384 | 385 | bool SpotifyArduino::playerControl(char *command, const char *deviceId, const char *body) 386 | { 387 | if (deviceId[0] != 0) 388 | { 389 | char *questionMarkPointer; 390 | questionMarkPointer = strchr(command, '?'); 391 | char deviceIdBuff[50]; 392 | if (questionMarkPointer == NULL) 393 | { 394 | sprintf(deviceIdBuff, "?device_id=%s", deviceId); 395 | } 396 | else 397 | { 398 | // params already started 399 | sprintf(deviceIdBuff, "&device_id=%s", deviceId); 400 | } 401 | strcat(command, deviceIdBuff); 402 | } 403 | 404 | #ifdef SPOTIFY_DEBUG 405 | Serial.println(command); 406 | Serial.println(body); 407 | #endif 408 | 409 | if (autoTokenRefresh) 410 | { 411 | checkAndRefreshAccessToken(); 412 | } 413 | int statusCode = makePutRequest(command, _bearerToken, body); 414 | 415 | closeClient(); 416 | //Will return 204 if all went well. 417 | return statusCode == 204; 418 | } 419 | 420 | bool SpotifyArduino::playerNavigate(char *command, const char *deviceId) 421 | { 422 | if (deviceId[0] != 0) 423 | { 424 | char deviceIdBuff[50]; 425 | sprintf(deviceIdBuff, "?device_id=%s", deviceId); 426 | strcat(command, deviceIdBuff); 427 | } 428 | 429 | #ifdef SPOTIFY_DEBUG 430 | Serial.println(command); 431 | #endif 432 | 433 | if (autoTokenRefresh) 434 | { 435 | checkAndRefreshAccessToken(); 436 | } 437 | int statusCode = makePostRequest(command, _bearerToken); 438 | 439 | closeClient(); 440 | //Will return 204 if all went well. 441 | return statusCode == 204; 442 | } 443 | 444 | bool SpotifyArduino::nextTrack(const char *deviceId) 445 | { 446 | char command[100] = SPOTIFY_NEXT_TRACK_ENDPOINT; 447 | return playerNavigate(command, deviceId); 448 | } 449 | 450 | bool SpotifyArduino::previousTrack(const char *deviceId) 451 | { 452 | char command[100] = SPOTIFY_PREVIOUS_TRACK_ENDPOINT; 453 | return playerNavigate(command, deviceId); 454 | } 455 | bool SpotifyArduino::seek(int position, const char *deviceId) 456 | { 457 | char command[100] = SPOTIFY_SEEK_ENDPOINT; 458 | char tempBuff[100]; 459 | sprintf(tempBuff, "?position_ms=%d", position); 460 | strcat(command, tempBuff); 461 | if (deviceId[0] != 0) 462 | { 463 | sprintf(tempBuff, "?device_id=%s", deviceId); 464 | strcat(command, tempBuff); 465 | } 466 | 467 | #ifdef SPOTIFY_DEBUG 468 | Serial.println(command); 469 | printStack(); 470 | #endif 471 | 472 | if (autoTokenRefresh) 473 | { 474 | checkAndRefreshAccessToken(); 475 | } 476 | int statusCode = makePutRequest(command, _bearerToken); 477 | closeClient(); 478 | //Will return 204 if all went well. 479 | return statusCode == 204; 480 | } 481 | 482 | bool SpotifyArduino::transferPlayback(const char *deviceId, bool play) 483 | { 484 | char body[100]; 485 | sprintf(body, "{\"device_ids\":[\"%s\"],\"play\":\"%s\"}", deviceId, (play ? "true" : "false")); 486 | 487 | #ifdef SPOTIFY_DEBUG 488 | Serial.println(SPOTIFY_PLAYER_ENDPOINT); 489 | Serial.println(body); 490 | printStack(); 491 | #endif 492 | 493 | if (autoTokenRefresh) 494 | { 495 | checkAndRefreshAccessToken(); 496 | } 497 | int statusCode = makePutRequest(SPOTIFY_PLAYER_ENDPOINT, _bearerToken, body); 498 | closeClient(); 499 | //Will return 204 if all went well. 500 | return statusCode == 204; 501 | } 502 | 503 | int SpotifyArduino::getCurrentlyPlaying(processCurrentlyPlaying currentlyPlayingCallback, const char *market) 504 | { 505 | char command[75] = SPOTIFY_CURRENTLY_PLAYING_ENDPOINT; 506 | if (market[0] != 0) 507 | { 508 | char marketBuff[15]; 509 | sprintf(marketBuff, "&market=%s", market); 510 | strcat(command, marketBuff); 511 | } 512 | 513 | #ifdef SPOTIFY_DEBUG 514 | Serial.println(command); 515 | printStack(); 516 | #endif 517 | 518 | // Get from https://arduinojson.org/v6/assistant/ 519 | const size_t bufferSize = currentlyPlayingBufferSize; 520 | 521 | if (autoTokenRefresh) 522 | { 523 | checkAndRefreshAccessToken(); 524 | } 525 | int statusCode = makeGetRequest(command, _bearerToken); 526 | #ifdef SPOTIFY_DEBUG 527 | Serial.print("Status Code: "); 528 | Serial.println(statusCode); 529 | printStack(); 530 | #endif 531 | if (statusCode > 0) 532 | { 533 | skipHeaders(); 534 | } 535 | 536 | if (statusCode == 200) 537 | { 538 | CurrentlyPlaying current; 539 | 540 | //Apply Json Filter: https://arduinojson.org/v6/example/filter/ 541 | StaticJsonDocument<464> filter; 542 | filter["is_playing"] = true; 543 | filter["currently_playing_type"] = true; 544 | filter["progress_ms"] = true; 545 | filter["context"]["uri"] = true; 546 | 547 | JsonObject filter_item = filter.createNestedObject("item"); 548 | filter_item["duration_ms"] = true; 549 | filter_item["name"] = true; 550 | filter_item["uri"] = true; 551 | 552 | JsonObject filter_item_artists_0 = filter_item["artists"].createNestedObject(); 553 | filter_item_artists_0["name"] = true; 554 | filter_item_artists_0["uri"] = true; 555 | 556 | JsonObject filter_item_album = filter_item.createNestedObject("album"); 557 | filter_item_album["name"] = true; 558 | filter_item_album["uri"] = true; 559 | 560 | JsonObject filter_item_album_images_0 = filter_item_album["images"].createNestedObject(); 561 | filter_item_album_images_0["height"] = true; 562 | filter_item_album_images_0["width"] = true; 563 | filter_item_album_images_0["url"] = true; 564 | 565 | // Podcast filters 566 | JsonObject filter_item_show = filter_item.createNestedObject("show"); 567 | filter_item_show["name"] = true; 568 | filter_item_show["uri"] = true; 569 | 570 | JsonObject filter_item_images_0 = filter_item["images"].createNestedObject(); 571 | filter_item_images_0["height"] = true; 572 | filter_item_images_0["width"] = true; 573 | filter_item_images_0["url"] = true; 574 | 575 | // Allocate DynamicJsonDocument 576 | DynamicJsonDocument doc(bufferSize); 577 | 578 | // Parse JSON object 579 | #ifndef SPOTIFY_PRINT_JSON_PARSE 580 | DeserializationError error = deserializeJson(doc, *client, DeserializationOption::Filter(filter)); 581 | #else 582 | ReadLoggingStream loggingStream(*client, Serial); 583 | DeserializationError error = deserializeJson(doc, loggingStream, DeserializationOption::Filter(filter)); 584 | #endif 585 | if (!error) 586 | { 587 | #ifdef SPOTIFY_DEBUG 588 | serializeJsonPretty(doc, Serial); 589 | #endif 590 | JsonObject item = doc["item"]; 591 | 592 | const char *currently_playing_type = doc["currently_playing_type"]; 593 | 594 | current.isPlaying = doc["is_playing"].as(); 595 | 596 | current.progressMs = doc["progress_ms"].as(); 597 | current.durationMs = item["duration_ms"].as(); 598 | 599 | // context may be null 600 | if (!doc["context"].isNull()) 601 | { 602 | current.contextUri = doc["context"]["uri"].as(); 603 | } 604 | else 605 | { 606 | current.contextUri = NULL; 607 | } 608 | 609 | // Check currently playing type 610 | if (strcmp(currently_playing_type, "track") == 0) 611 | { 612 | current.currentlyPlayingType = track; 613 | } 614 | else if (strcmp(currently_playing_type, "episode") == 0) 615 | { 616 | current.currentlyPlayingType = episode; 617 | } 618 | else 619 | { 620 | current.currentlyPlayingType = other; 621 | } 622 | 623 | // If it's a song/track 624 | if (current.currentlyPlayingType == track) 625 | { 626 | int numArtists = item["artists"].size(); 627 | if (numArtists > SPOTIFY_MAX_NUM_ARTISTS) 628 | { 629 | numArtists = SPOTIFY_MAX_NUM_ARTISTS; 630 | } 631 | current.numArtists = numArtists; 632 | 633 | for (int i = 0; i < current.numArtists; i++) 634 | { 635 | current.artists[i].artistName = item["artists"][i]["name"].as(); 636 | current.artists[i].artistUri = item["artists"][i]["uri"].as(); 637 | } 638 | 639 | current.albumName = item["album"]["name"].as(); 640 | current.albumUri = item["album"]["uri"].as(); 641 | 642 | JsonArray images = item["album"]["images"]; 643 | 644 | // Images are returned in order of width, so last should be smallest. 645 | int numImages = images.size(); 646 | int startingIndex = 0; 647 | if (numImages > SPOTIFY_NUM_ALBUM_IMAGES) 648 | { 649 | startingIndex = numImages - SPOTIFY_NUM_ALBUM_IMAGES; 650 | current.numImages = SPOTIFY_NUM_ALBUM_IMAGES; 651 | } 652 | else 653 | { 654 | current.numImages = numImages; 655 | } 656 | #ifdef SPOTIFY_DEBUG 657 | Serial.print(F("Num Images: ")); 658 | Serial.println(current.numImages); 659 | Serial.println(numImages); 660 | #endif 661 | 662 | for (int i = 0; i < current.numImages; i++) 663 | { 664 | int adjustedIndex = startingIndex + i; 665 | current.albumImages[i].height = images[adjustedIndex]["height"].as(); 666 | current.albumImages[i].width = images[adjustedIndex]["width"].as(); 667 | current.albumImages[i].url = images[adjustedIndex]["url"].as(); 668 | } 669 | 670 | current.trackName = item["name"].as(); 671 | current.trackUri = item["uri"].as(); 672 | } 673 | else if (current.currentlyPlayingType == episode) // Podcast 674 | { 675 | current.numArtists = 1; 676 | 677 | // Save Podcast as the "track" 678 | current.trackName = item["name"].as(); 679 | current.trackUri = item["uri"].as(); 680 | 681 | // Save Show name as the "artist" 682 | current.artists[0].artistName = item["show"]["name"].as(); 683 | current.artists[0].artistUri = item["show"]["uri"].as(); 684 | 685 | // Leave "album" name blank 686 | char blank[1] = ""; 687 | current.albumName = blank; 688 | current.albumUri = blank; 689 | 690 | // Save the episode images as the "album art" 691 | JsonArray images = item["images"]; 692 | // Images are returned in order of width, so last should be smallest. 693 | int numImages = images.size(); 694 | int startingIndex = 0; 695 | if (numImages > SPOTIFY_NUM_ALBUM_IMAGES) 696 | { 697 | startingIndex = numImages - SPOTIFY_NUM_ALBUM_IMAGES; 698 | current.numImages = SPOTIFY_NUM_ALBUM_IMAGES; 699 | } 700 | else 701 | { 702 | current.numImages = numImages; 703 | } 704 | #ifdef SPOTIFY_DEBUG 705 | Serial.print(F("Num Images: ")); 706 | Serial.println(current.numImages); 707 | Serial.println(numImages); 708 | #endif 709 | 710 | for (int i = 0; i < current.numImages; i++) 711 | { 712 | int adjustedIndex = startingIndex + i; 713 | current.albumImages[i].height = images[adjustedIndex]["height"].as(); 714 | current.albumImages[i].width = images[adjustedIndex]["width"].as(); 715 | current.albumImages[i].url = images[adjustedIndex]["url"].as(); 716 | } 717 | } 718 | 719 | currentlyPlayingCallback(current); 720 | } 721 | else 722 | { 723 | #ifdef SPOTIFY_SERIAL_OUTPUT 724 | Serial.print(F("deserializeJson() failed with code ")); 725 | Serial.println(error.c_str()); 726 | #endif 727 | statusCode = -1; 728 | } 729 | } 730 | 731 | closeClient(); 732 | return statusCode; 733 | } 734 | 735 | int SpotifyArduino::getPlayerDetails(processPlayerDetails playerDetailsCallback, const char *market) 736 | { 737 | char command[100] = SPOTIFY_PLAYER_ENDPOINT; 738 | if (market[0] != 0) 739 | { 740 | char marketBuff[30]; 741 | sprintf(marketBuff, "?market=%s", market); 742 | strcat(command, marketBuff); 743 | } 744 | 745 | #ifdef SPOTIFY_DEBUG 746 | Serial.println(command); 747 | printStack(); 748 | #endif 749 | 750 | // Get from https://arduinojson.org/v6/assistant/ 751 | const size_t bufferSize = playerDetailsBufferSize; 752 | if (autoTokenRefresh) 753 | { 754 | checkAndRefreshAccessToken(); 755 | } 756 | 757 | int statusCode = makeGetRequest(command, _bearerToken); 758 | #ifdef SPOTIFY_DEBUG 759 | Serial.print("Status Code: "); 760 | Serial.println(statusCode); 761 | #endif 762 | if (statusCode > 0) 763 | { 764 | skipHeaders(); 765 | } 766 | 767 | if (statusCode == 200) 768 | { 769 | 770 | StaticJsonDocument<192> filter; 771 | JsonObject filter_device = filter.createNestedObject("device"); 772 | filter_device["id"] = true; 773 | filter_device["name"] = true; 774 | filter_device["type"] = true; 775 | filter_device["is_active"] = true; 776 | filter_device["is_private_session"] = true; 777 | filter_device["is_restricted"] = true; 778 | filter_device["volume_percent"] = true; 779 | filter["progress_ms"] = true; 780 | filter["is_playing"] = true; 781 | filter["shuffle_state"] = true; 782 | filter["repeat_state"] = true; 783 | 784 | // Allocate DynamicJsonDocument 785 | DynamicJsonDocument doc(bufferSize); 786 | 787 | // Parse JSON object 788 | #ifndef SPOTIFY_PRINT_JSON_PARSE 789 | DeserializationError error = deserializeJson(doc, *client, DeserializationOption::Filter(filter)); 790 | #else 791 | ReadLoggingStream loggingStream(*client, Serial); 792 | DeserializationError error = deserializeJson(doc, loggingStream, DeserializationOption::Filter(filter)); 793 | #endif 794 | if (!error) 795 | { 796 | PlayerDetails playerDetails; 797 | 798 | JsonObject device = doc["device"]; 799 | // Copy into buffer and make the last character a null just incase we went over. 800 | playerDetails.device.id = device["id"].as(); 801 | playerDetails.device.name = device["name"].as(); 802 | playerDetails.device.type = device["type"].as(); 803 | 804 | playerDetails.device.isActive = device["is_active"].as(); 805 | playerDetails.device.isPrivateSession = device["is_private_session"].as(); 806 | playerDetails.device.isRestricted = device["is_restricted"].as(); 807 | playerDetails.device.volumePercent = device["volume_percent"].as(); 808 | 809 | playerDetails.progressMs = doc["progress_ms"].as(); 810 | playerDetails.isPlaying = doc["is_playing"].as(); 811 | 812 | playerDetails.shuffleState = doc["shuffle_state"].as(); 813 | 814 | const char *repeat_state = doc["repeat_state"]; 815 | 816 | if (strncmp(repeat_state, "track", 5) == 0) 817 | { 818 | playerDetails.repeateState = repeat_track; 819 | } 820 | else if (strncmp(repeat_state, "context", 7) == 0) 821 | { 822 | playerDetails.repeateState = repeat_context; 823 | } 824 | else 825 | { 826 | playerDetails.repeateState = repeat_off; 827 | } 828 | 829 | playerDetailsCallback(playerDetails); 830 | } 831 | else 832 | { 833 | #ifdef SPOTIFY_SERIAL_OUTPUT 834 | Serial.print(F("deserializeJson() failed with code ")); 835 | Serial.println(error.c_str()); 836 | #endif 837 | statusCode = -1; 838 | } 839 | } 840 | 841 | closeClient(); 842 | return statusCode; 843 | } 844 | 845 | int SpotifyArduino::getDevices(processDevices devicesCallback) 846 | { 847 | 848 | #ifdef SPOTIFY_DEBUG 849 | Serial.println(SPOTIFY_DEVICES_ENDPOINT); 850 | printStack(); 851 | #endif 852 | 853 | // Get from https://arduinojson.org/v6/assistant/ 854 | const size_t bufferSize = getDevicesBufferSize; 855 | if (autoTokenRefresh) 856 | { 857 | checkAndRefreshAccessToken(); 858 | } 859 | 860 | int statusCode = makeGetRequest(SPOTIFY_DEVICES_ENDPOINT, _bearerToken); 861 | #ifdef SPOTIFY_DEBUG 862 | Serial.print("Status Code: "); 863 | Serial.println(statusCode); 864 | #endif 865 | if (statusCode > 0) 866 | { 867 | skipHeaders(); 868 | } 869 | 870 | if (statusCode == 200) 871 | { 872 | 873 | // Allocate DynamicJsonDocument 874 | DynamicJsonDocument doc(bufferSize); 875 | 876 | // Parse JSON object 877 | #ifndef SPOTIFY_PRINT_JSON_PARSE 878 | DeserializationError error = deserializeJson(doc, *client); 879 | #else 880 | ReadLoggingStream loggingStream(*client, Serial); 881 | DeserializationError error = deserializeJson(doc, loggingStream); 882 | #endif 883 | if (!error) 884 | { 885 | 886 | uint8_t totalDevices = doc["devices"].size(); 887 | 888 | SpotifyDevice spotifyDevice; 889 | for (int i = 0; i < totalDevices; i++) 890 | { 891 | JsonObject device = doc["devices"][i]; 892 | spotifyDevice.id = device["id"].as(); 893 | spotifyDevice.name = device["name"].as(); 894 | spotifyDevice.type = device["type"].as(); 895 | 896 | spotifyDevice.isActive = device["is_active"].as(); 897 | spotifyDevice.isPrivateSession = device["is_private_session"].as(); 898 | spotifyDevice.isRestricted = device["is_restricted"].as(); 899 | spotifyDevice.volumePercent = device["volume_percent"].as(); 900 | 901 | if (!devicesCallback(spotifyDevice, i, totalDevices)) 902 | { 903 | //User has indicated they are finished. 904 | break; 905 | } 906 | } 907 | } 908 | else 909 | { 910 | #ifdef SPOTIFY_SERIAL_OUTPUT 911 | Serial.print(F("deserializeJson() failed with code ")); 912 | Serial.println(error.c_str()); 913 | #endif 914 | statusCode = -1; 915 | } 916 | } 917 | 918 | closeClient(); 919 | return statusCode; 920 | } 921 | 922 | int SpotifyArduino::searchForSong(String query, int limit, processSearch searchCallback, SearchResult results[]) 923 | { 924 | 925 | #ifdef SPOTIFY_DEBUG 926 | Serial.println(SPOTIFY_SEARCH_ENDPOINT); 927 | printStack(); 928 | #endif 929 | 930 | // Get from https://arduinojson.org/v6/assistant/ 931 | const size_t bufferSize = searchDetailsBufferSize; 932 | if (autoTokenRefresh) 933 | { 934 | checkAndRefreshAccessToken(); 935 | } 936 | 937 | int statusCode = makeGetRequest((SPOTIFY_SEARCH_ENDPOINT + query + "&limit=" + limit).c_str(), _bearerToken); 938 | #ifdef SPOTIFY_DEBUG 939 | Serial.print("Status Code: "); 940 | Serial.println(statusCode); 941 | #endif 942 | if (statusCode > 0) 943 | { 944 | skipHeaders(); 945 | } 946 | 947 | if (statusCode == 200) 948 | { 949 | 950 | // Allocate DynamicJsonDocument 951 | DynamicJsonDocument doc(bufferSize); 952 | 953 | // Parse JSON object 954 | #ifndef SPOTIFY_PRINT_JSON_PARSE 955 | DeserializationError error = deserializeJson(doc, *client); 956 | #else 957 | ReadLoggingStream loggingStream(*client, Serial); 958 | DeserializationError error = deserializeJson(doc, loggingStream); 959 | #endif 960 | if (!error) 961 | { 962 | 963 | uint8_t totalResults = doc["tracks"]["items"].size(); 964 | 965 | Serial.print("Total Results: "); 966 | Serial.println(totalResults); 967 | 968 | SearchResult searchResult; 969 | for (int i = 0; i < totalResults; i++) 970 | { 971 | //Polling track information 972 | JsonObject result = doc["tracks"]["items"][i]; 973 | searchResult.trackUri = result["uri"].as(); 974 | searchResult.trackName = result["name"].as(); 975 | searchResult.albumUri = result["album"]["uri"].as(); 976 | searchResult.albumName = result["album"]["name"].as(); 977 | 978 | //Pull artist Information for the result 979 | uint8_t totalArtists = result["artists"].size(); 980 | searchResult.numArtists = totalArtists; 981 | 982 | SpotifyArtist artist; 983 | for (int j = 0; j < totalArtists; j++) 984 | { 985 | JsonObject artistResult = result["artists"][j]; 986 | artist.artistName = artistResult["name"].as(); 987 | artist.artistUri = artistResult["uri"].as(); 988 | searchResult.artists[j] = artist; 989 | } 990 | 991 | uint8_t totalImages = result["album"]["images"].size(); 992 | searchResult.numImages = totalImages; 993 | 994 | SpotifyImage image; 995 | for (int j = 0; j < totalImages; j++) 996 | { 997 | JsonObject imageResult = result["album"]["images"][j]; 998 | image.height = imageResult["height"].as(); 999 | image.width = imageResult["width"].as(); 1000 | image.url = imageResult["url"].as(); 1001 | searchResult.albumImages[j] = image; 1002 | } 1003 | 1004 | //Serial.println(searchResult.trackName); 1005 | results[i] = searchResult; 1006 | 1007 | if (i >= limit || !searchCallback(searchResult, i, totalResults)) 1008 | { 1009 | //Break at the limit or when indicated 1010 | break; 1011 | } 1012 | } 1013 | } 1014 | else 1015 | { 1016 | #ifdef SPOTIFY_SERIAL_OUTPUT 1017 | Serial.print(F("deserializeJson() failed with code ")); 1018 | Serial.println(error.c_str()); 1019 | #endif 1020 | statusCode = -1; 1021 | } 1022 | } 1023 | 1024 | closeClient(); 1025 | return statusCode; 1026 | } 1027 | 1028 | int SpotifyArduino::commonGetImage(char *imageUrl) 1029 | { 1030 | #ifdef SPOTIFY_DEBUG 1031 | Serial.print(F("Parsing image URL: ")); 1032 | Serial.println(imageUrl); 1033 | #endif 1034 | 1035 | uint8_t lengthOfString = strlen(imageUrl); 1036 | 1037 | // We are going to just assume https, that's all I've 1038 | // seen and I can't imagine a company will go back 1039 | // to http 1040 | 1041 | if (strncmp(imageUrl, "https://", 8) != 0) 1042 | { 1043 | #ifdef SPOTIFY_SERIAL_OUTPUT 1044 | Serial.print(F("Url not in expected format: ")); 1045 | Serial.println(imageUrl); 1046 | Serial.println("(expected it to start with \"https://\")"); 1047 | #endif 1048 | return false; 1049 | } 1050 | 1051 | uint8_t protocolLength = 8; 1052 | 1053 | char *pathStart = strchr(imageUrl + protocolLength, '/'); 1054 | uint8_t pathIndex = pathStart - imageUrl; 1055 | uint8_t pathLength = lengthOfString - pathIndex; 1056 | char path[pathLength + 1]; 1057 | strncpy(path, pathStart, pathLength); 1058 | path[pathLength] = '\0'; 1059 | 1060 | uint8_t hostLength = pathIndex - protocolLength; 1061 | char host[hostLength + 1]; 1062 | strncpy(host, imageUrl + protocolLength, hostLength); 1063 | host[hostLength] = '\0'; 1064 | 1065 | #ifdef SPOTIFY_DEBUG 1066 | 1067 | Serial.print(F("host: ")); 1068 | Serial.println(host); 1069 | 1070 | Serial.print(F("len:host:")); 1071 | Serial.println(hostLength); 1072 | 1073 | Serial.print(F("path: ")); 1074 | Serial.println(path); 1075 | 1076 | Serial.print(F("len:path: ")); 1077 | Serial.println(strlen(path)); 1078 | #endif 1079 | 1080 | int statusCode = makeGetRequest(path, NULL, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", host); 1081 | #ifdef SPOTIFY_DEBUG 1082 | Serial.print(F("statusCode: ")); 1083 | Serial.println(statusCode); 1084 | #endif 1085 | if (statusCode == 200) 1086 | { 1087 | return getContentLength(); 1088 | } 1089 | 1090 | // Failed 1091 | return -1; 1092 | } 1093 | 1094 | bool SpotifyArduino::getImage(char *imageUrl, Stream *file) 1095 | { 1096 | int totalLength = commonGetImage(imageUrl); 1097 | 1098 | #ifdef SPOTIFY_DEBUG 1099 | Serial.print(F("file length: ")); 1100 | Serial.println(totalLength); 1101 | #endif 1102 | if (totalLength > 0) 1103 | { 1104 | skipHeaders(false); 1105 | int remaining = totalLength; 1106 | // This section of code is inspired but the "Web_Jpg" 1107 | // example of TJpg_Decoder 1108 | // https://github.com/Bodmer/TJpg_Decoder 1109 | // ----------- 1110 | uint8_t buff[128] = {0}; 1111 | while (client->connected() && (remaining > 0 || remaining == -1)) 1112 | { 1113 | // Get available data size 1114 | size_t size = client->available(); 1115 | 1116 | if (size) 1117 | { 1118 | // Read up to 128 bytes 1119 | int c = client->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size)); 1120 | 1121 | // Write it to file 1122 | file->write(buff, c); 1123 | 1124 | // Calculate remaining bytes 1125 | if (remaining > 0) 1126 | { 1127 | remaining -= c; 1128 | } 1129 | } 1130 | yield(); 1131 | } 1132 | // --------- 1133 | #ifdef SPOTIFY_DEBUG 1134 | Serial.println(F("Finished getting image")); 1135 | #endif 1136 | } 1137 | 1138 | closeClient(); 1139 | 1140 | return (totalLength > 0); //Probably could be improved! 1141 | } 1142 | 1143 | bool SpotifyArduino::getImage(char *imageUrl, uint8_t **image, int *imageLength) 1144 | { 1145 | int totalLength = commonGetImage(imageUrl); 1146 | 1147 | #ifdef SPOTIFY_DEBUG 1148 | Serial.print(F("file length: ")); 1149 | Serial.println(totalLength); 1150 | #endif 1151 | if (totalLength > 0) 1152 | { 1153 | skipHeaders(false); 1154 | uint8_t *imgPtr = (uint8_t *)malloc(totalLength); 1155 | *image = imgPtr; 1156 | *imageLength = totalLength; 1157 | int remaining = totalLength; 1158 | int amountRead = 0; 1159 | 1160 | #ifdef SPOTIFY_DEBUG 1161 | Serial.println(F("Fetching Image")); 1162 | #endif 1163 | 1164 | // This section of code is inspired but the "Web_Jpg" 1165 | // example of TJpg_Decoder 1166 | // https://github.com/Bodmer/TJpg_Decoder 1167 | // ----------- 1168 | uint8_t buff[128] = {0}; 1169 | while (client->connected() && (remaining > 0 || remaining == -1)) 1170 | { 1171 | // Get available data size 1172 | size_t size = client->available(); 1173 | 1174 | if (size) 1175 | { 1176 | // Read up to 128 bytes 1177 | int c = client->readBytes(buff, ((size > sizeof(buff)) ? sizeof(buff) : size)); 1178 | 1179 | // Write it to file 1180 | memcpy((uint8_t *)imgPtr + amountRead, (uint8_t *)buff, c); 1181 | 1182 | // Calculate remaining bytes 1183 | if (remaining > 0) 1184 | { 1185 | amountRead += c; 1186 | remaining -= c; 1187 | } 1188 | } 1189 | yield(); 1190 | } 1191 | // --------- 1192 | #ifdef SPOTIFY_DEBUG 1193 | Serial.println(F("Finished getting image")); 1194 | #endif 1195 | } 1196 | 1197 | closeClient(); 1198 | 1199 | return (totalLength > 0); //Probably could be improved! 1200 | } 1201 | 1202 | int SpotifyArduino::getContentLength() 1203 | { 1204 | 1205 | if (client->find("Content-Length:")) 1206 | { 1207 | int contentLength = client->parseInt(); 1208 | #ifdef SPOTIFY_DEBUG 1209 | Serial.print(F("Content-Length: ")); 1210 | Serial.println(contentLength); 1211 | #endif 1212 | return contentLength; 1213 | } 1214 | 1215 | return -1; 1216 | } 1217 | 1218 | void SpotifyArduino::skipHeaders(bool tossUnexpectedForJSON) 1219 | { 1220 | // Skip HTTP headers 1221 | if (!client->find("\r\n\r\n")) 1222 | { 1223 | #ifdef SPOTIFY_SERIAL_OUTPUT 1224 | Serial.println(F("Invalid response")); 1225 | #endif 1226 | return; 1227 | } 1228 | 1229 | if (tossUnexpectedForJSON) 1230 | { 1231 | // Was getting stray characters between the headers and the body 1232 | // This should toss them away 1233 | while (client->available() && client->peek() != '{') 1234 | { 1235 | char c = 0; 1236 | client->readBytes(&c, 1); 1237 | #ifdef SPOTIFY_DEBUG 1238 | Serial.print(F("Tossing an unexpected character: ")); 1239 | Serial.println(c); 1240 | #endif 1241 | } 1242 | } 1243 | } 1244 | 1245 | int SpotifyArduino::getHttpStatusCode() 1246 | { 1247 | char status[32] = {0}; 1248 | client->readBytesUntil('\r', status, sizeof(status)); 1249 | #ifdef SPOTIFY_DEBUG 1250 | Serial.print(F("Status: ")); 1251 | Serial.println(status); 1252 | #endif 1253 | 1254 | char *token; 1255 | token = strtok(status, " "); // https://www.tutorialspoint.com/c_standard_library/c_function_strtok.htm 1256 | 1257 | #ifdef SPOTIFY_DEBUG 1258 | Serial.print(F("HTTP Version: ")); 1259 | Serial.println(token); 1260 | #endif 1261 | 1262 | if (token != NULL && (strcmp(token, "HTTP/1.0") == 0 || strcmp(token, "HTTP/1.1") == 0)) 1263 | { 1264 | token = strtok(NULL, " "); 1265 | if (token != NULL) 1266 | { 1267 | #ifdef SPOTIFY_DEBUG 1268 | Serial.print(F("Status Code: ")); 1269 | Serial.println(token); 1270 | #endif 1271 | return atoi(token); 1272 | } 1273 | } 1274 | 1275 | return -1; 1276 | } 1277 | 1278 | void SpotifyArduino::parseError() 1279 | { 1280 | //This method doesn't currently do anything other than print 1281 | #ifdef SPOTIFY_SERIAL_OUTPUT 1282 | DynamicJsonDocument doc(1000); 1283 | DeserializationError error = deserializeJson(doc, *client); 1284 | if (!error) 1285 | { 1286 | Serial.print(F("getAuthToken error")); 1287 | serializeJson(doc, Serial); 1288 | } 1289 | else 1290 | { 1291 | Serial.print(F("Could not parse error")); 1292 | } 1293 | #endif 1294 | } 1295 | 1296 | void SpotifyArduino::lateInit(const char *clientId, const char *clientSecret, const char *refreshToken) 1297 | { 1298 | this->_clientId = clientId; 1299 | this->_clientSecret = clientSecret; 1300 | setRefreshToken(refreshToken); 1301 | } 1302 | 1303 | void SpotifyArduino::closeClient() 1304 | { 1305 | if (client->connected()) 1306 | { 1307 | #ifdef SPOTIFY_DEBUG 1308 | Serial.println(F("Closing client")); 1309 | #endif 1310 | client->stop(); 1311 | } 1312 | } 1313 | 1314 | #ifdef SPOTIFY_DEBUG 1315 | void SpotifyArduino::printStack() 1316 | { 1317 | char stack; 1318 | Serial.print(F("stack size ")); 1319 | Serial.println(stack_start - &stack); 1320 | } 1321 | #endif 1322 | -------------------------------------------------------------------------------- /src/SpotifyArduino.h: -------------------------------------------------------------------------------- 1 | /* 2 | SpotifyArduino - An Arduino library to wrap the Spotify API 3 | 4 | Copyright (c) 2020 Brian Lough. 5 | 6 | This library is free software; you can redistribute it and/or 7 | modify it under the terms of the GNU Lesser General Public 8 | License as published by the Free Software Foundation; either 9 | version 2.1 of the License, or (at your option) any later version. 10 | 11 | This library is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Lesser General Public License for more details. 15 | 16 | You should have received a copy of the GNU Lesser General Public 17 | License along with this library; if not, write to the Free Software 18 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 19 | */ 20 | 21 | #ifndef SpotifyArduino_h 22 | #define SpotifyArduino_h 23 | 24 | // I find setting these types of flags unreliable from the Arduino IDE 25 | // so uncomment this if its not working for you. 26 | // NOTE: Do not use this option on live-streams, it will reveal your 27 | // private tokens! 28 | 29 | #define SPOTIFY_DEBUG 1 30 | 31 | // Comment out if you want to disable any serial output from this library (also comment out DEBUG and PRINT_JSON_PARSE) 32 | #define SPOTIFY_SERIAL_OUTPUT 1 33 | 34 | // Prints the JSON received to serial (only use for debugging as it will be slow) 35 | //#define SPOTIFY_PRINT_JSON_PARSE 1 36 | 37 | #include 38 | #include 39 | #include 40 | 41 | #ifdef SPOTIFY_PRINT_JSON_PARSE 42 | #include 43 | #endif 44 | 45 | #define SPOTIFY_HOST "api.spotify.com" 46 | #define SPOTIFY_ACCOUNTS_HOST "accounts.spotify.com" 47 | 48 | // Fingerprint for "*.spotify.com", correct as of March 14, 2024 49 | #define SPOTIFY_FINGERPRINT "69 2B 36 29 F0 B5 FC 1B A3 57 A6 76 E6 92 EF 30 14 22 34 6A" 50 | 51 | // Fingerprint for "*.scdn.co", correct as of March 14, 2024 52 | #define SPOTIFY_IMAGE_SERVER_FINGERPRINT "0A 0F 59 45 2C FF 37 3C FE 37 27 AD 32 64 59 A9 5A B6 2F 30" 53 | 54 | #define SPOTIFY_TIMEOUT 2000 55 | 56 | #define SPOTIFY_NAME_CHAR_LENGTH 100 //Increase if artists/song/album names are being cut off 57 | #define SPOTIFY_URI_CHAR_LENGTH 40 58 | #define SPOTIFY_URL_CHAR_LENGTH 70 59 | 60 | #define SPOTIFY_DEVICE_ID_CHAR_LENGTH 45 61 | #define SPOTIFY_DEVICE_NAME_CHAR_LENGTH 80 62 | #define SPOTIFY_DEVICE_TYPE_CHAR_LENGTH 30 63 | 64 | #define SPOTIFY_CURRENTLY_PLAYING_ENDPOINT "/v1/me/player/currently-playing?additional_types=episode" 65 | 66 | #define SPOTIFY_PLAYER_ENDPOINT "/v1/me/player" 67 | #define SPOTIFY_DEVICES_ENDPOINT "/v1/me/player/devices" 68 | 69 | #define SPOTIFY_PLAY_ENDPOINT "/v1/me/player/play" 70 | #define SPOTIFY_SEARCH_ENDPOINT "/v1/search" 71 | #define SPOTIFY_PAUSE_ENDPOINT "/v1/me/player/pause" 72 | #define SPOTIFY_VOLUME_ENDPOINT "/v1/me/player/volume?volume_percent=%d" 73 | #define SPOTIFY_SHUFFLE_ENDPOINT "/v1/me/player/shuffle?state=%s" 74 | #define SPOTIFY_REPEAT_ENDPOINT "/v1/me/player/repeat?state=%s" 75 | 76 | #define SPOTIFY_NEXT_TRACK_ENDPOINT "/v1/me/player/next" 77 | #define SPOTIFY_PREVIOUS_TRACK_ENDPOINT "/v1/me/player/previous" 78 | 79 | #define SPOTIFY_SEEK_ENDPOINT "/v1/me/player/seek" 80 | 81 | #define SPOTIFY_TOKEN_ENDPOINT "/api/token" 82 | 83 | #define SPOTIFY_NUM_ALBUM_IMAGES 3 // Max spotify returns is 3, but the third one is probably too big for an ESP 84 | 85 | #define SPOTIFY_MAX_NUM_ARTISTS 5 86 | 87 | #define SPOTIFY_ACCESS_TOKEN_LENGTH 309 88 | 89 | enum RepeatOptions 90 | { 91 | repeat_track, 92 | repeat_context, 93 | repeat_off 94 | }; 95 | 96 | enum SpotifyPlayingType 97 | { 98 | track, 99 | episode, 100 | other 101 | }; 102 | 103 | struct SpotifyImage 104 | { 105 | int height; 106 | int width; 107 | const char *url; 108 | }; 109 | 110 | struct SpotifyDevice 111 | { 112 | const char *id; 113 | const char *name; 114 | const char *type; 115 | bool isActive; 116 | bool isRestricted; 117 | bool isPrivateSession; 118 | int volumePercent; 119 | }; 120 | 121 | struct PlayerDetails 122 | { 123 | SpotifyDevice device; 124 | 125 | long progressMs; 126 | bool isPlaying; 127 | RepeatOptions repeateState; 128 | bool shuffleState; 129 | }; 130 | 131 | struct SpotifyArtist 132 | { 133 | const char *artistName; 134 | const char *artistUri; 135 | }; 136 | 137 | struct SearchResult 138 | { 139 | const char *albumName; 140 | const char *albumUri; 141 | const char *trackName; 142 | const char *trackUri; 143 | SpotifyArtist artists[SPOTIFY_MAX_NUM_ARTISTS]; 144 | SpotifyImage albumImages[SPOTIFY_NUM_ALBUM_IMAGES]; 145 | int numArtists; 146 | int numImages; 147 | }; 148 | 149 | struct CurrentlyPlaying 150 | { 151 | SpotifyArtist artists[SPOTIFY_MAX_NUM_ARTISTS]; 152 | int numArtists; 153 | const char *albumName; 154 | const char *albumUri; 155 | const char *trackName; 156 | const char *trackUri; 157 | SpotifyImage albumImages[SPOTIFY_NUM_ALBUM_IMAGES]; 158 | int numImages; 159 | bool isPlaying; 160 | long progressMs; 161 | long durationMs; 162 | const char *contextUri; 163 | SpotifyPlayingType currentlyPlayingType; 164 | }; 165 | 166 | typedef void (*processCurrentlyPlaying)(CurrentlyPlaying currentlyPlaying); 167 | typedef void (*processPlayerDetails)(PlayerDetails playerDetails); 168 | typedef bool (*processDevices)(SpotifyDevice device, int index, int numDevices); 169 | typedef bool (*processSearch)(SearchResult result, int index, int numResults); 170 | 171 | class SpotifyArduino 172 | { 173 | public: 174 | SpotifyArduino(Client &client); 175 | SpotifyArduino(Client &client, char *bearerToken); 176 | SpotifyArduino(Client &client, const char *clientId, const char *clientSecret, const char *refreshToken = ""); 177 | 178 | // Auth Methods 179 | void setRefreshToken(const char *refreshToken); 180 | bool refreshAccessToken(); 181 | bool checkAndRefreshAccessToken(); 182 | const char *requestAccessTokens(const char *code, const char *redirectUrl); 183 | 184 | // Generic Request Methods 185 | int makeGetRequest(const char *command, const char *authorization, const char *accept = "application/json", const char *host = SPOTIFY_HOST); 186 | int makeRequestWithBody(const char *type, const char *command, const char *authorization, const char *body = "", const char *contentType = "application/json", const char *host = SPOTIFY_HOST); 187 | int makePostRequest(const char *command, const char *authorization, const char *body = "", const char *contentType = "application/json", const char *host = SPOTIFY_HOST); 188 | int makePutRequest(const char *command, const char *authorization, const char *body = "", const char *contentType = "application/json", const char *host = SPOTIFY_HOST); 189 | 190 | // User methods 191 | int getCurrentlyPlaying(processCurrentlyPlaying currentlyPlayingCallback, const char *market = ""); 192 | int getPlayerDetails(processPlayerDetails playerDetailsCallback, const char *market = ""); 193 | int getDevices(processDevices devicesCallback); 194 | bool play(const char *deviceId = ""); 195 | bool playAdvanced(char *body, const char *deviceId = ""); 196 | bool pause(const char *deviceId = ""); 197 | bool setVolume(int volume, const char *deviceId = ""); 198 | bool toggleShuffle(bool shuffle, const char *deviceId = ""); 199 | bool setRepeatMode(RepeatOptions repeat, const char *deviceId = ""); 200 | bool nextTrack(const char *deviceId = ""); 201 | bool previousTrack(const char *deviceId = ""); 202 | bool playerControl(char *command, const char *deviceId = "", const char *body = ""); 203 | bool playerNavigate(char *command, const char *deviceId = ""); 204 | bool seek(int position, const char *deviceId = ""); 205 | bool transferPlayback(const char *deviceId, bool play = false); 206 | 207 | //Search 208 | int searchForSong(String query, int limit, processSearch searchCallback, SearchResult results[]); 209 | 210 | // Image methods 211 | bool getImage(char *imageUrl, Stream *file); 212 | bool getImage(char *imageUrl, uint8_t **image, int *imageLength); 213 | 214 | int portNumber = 443; 215 | int currentlyPlayingBufferSize = 3000; 216 | int playerDetailsBufferSize = 2000; 217 | int getDevicesBufferSize = 3000; 218 | int searchDetailsBufferSize = 3000; 219 | bool autoTokenRefresh = true; 220 | Client *client; 221 | void lateInit(const char *clientId, const char *clientSecret, const char *refreshToken = ""); 222 | 223 | #ifdef SPOTIFY_DEBUG 224 | char *stack_start; 225 | #endif 226 | 227 | private: 228 | char _bearerToken[SPOTIFY_ACCESS_TOKEN_LENGTH + 10]; //10 extra is for "bearer " at the start 229 | char *_refreshToken; 230 | const char *_clientId; 231 | const char *_clientSecret; 232 | unsigned int timeTokenRefreshed; 233 | unsigned int tokenTimeToLiveMs; 234 | int commonGetImage(char *imageUrl); 235 | int getContentLength(); 236 | int getHttpStatusCode(); 237 | void skipHeaders(bool tossUnexpectedForJSON = true); 238 | void closeClient(); 239 | void parseError(); 240 | const char *requestAccessTokensBody = 241 | R"(grant_type=authorization_code&code=%s&redirect_uri=%s&client_id=%s&client_secret=%s)"; 242 | const char *refreshAccessTokensBody = 243 | R"(grant_type=refresh_token&refresh_token=%s&client_id=%s&client_secret=%s)"; 244 | #ifdef SPOTIFY_DEBUG 245 | void printStack(); 246 | #endif 247 | }; 248 | 249 | #endif 250 | -------------------------------------------------------------------------------- /src/SpotifyArduinoCert.h: -------------------------------------------------------------------------------- 1 | // DigiCert Global Root CA Cert - as of 01/10/2023 2 | const char *spotify_server_cert = "-----BEGIN CERTIFICATE-----\n" 3 | "MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n" 4 | "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" 5 | "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n" 6 | "MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n" 7 | "MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" 8 | "b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n" 9 | "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n" 10 | "2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n" 11 | "1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n" 12 | "q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n" 13 | "tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n" 14 | "vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n" 15 | "BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n" 16 | "5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n" 17 | "1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n" 18 | "NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n" 19 | "Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n" 20 | "8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n" 21 | "pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n" 22 | "MrY=\n" 23 | "-----END CERTIFICATE-----\n"; 24 | 25 | // DigiCert Global Root G2 Cert - as of 01/10/2023 26 | const char *spotify_image_server_cert = "-----BEGIN CERTIFICATE-----\n" 27 | "MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh\n" 28 | "MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3\n" 29 | "d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH\n" 30 | "MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT\n" 31 | "MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j\n" 32 | "b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG\n" 33 | "9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI\n" 34 | "2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx\n" 35 | "1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ\n" 36 | "q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz\n" 37 | "tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ\n" 38 | "vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP\n" 39 | "BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV\n" 40 | "5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY\n" 41 | "1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4\n" 42 | "NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG\n" 43 | "Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91\n" 44 | "8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe\n" 45 | "pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl\n" 46 | "MrY=\n" 47 | "-----END CERTIFICATE-----\n"; 48 | --------------------------------------------------------------------------------