This commit is contained in:
Avihay Menahem
2024-06-20 16:05:45 +03:00
commit bd7931a9ad
5 changed files with 1199 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
build
.vscode

8
README.md Normal file
View File

@@ -0,0 +1,8 @@
# Spotify Remote Controller
## Overview
The Spotify Remote Controller is an ESP32 project that allows you to control your Spotify playback using a custom hardware interface. It features an AMOLED display, WiFi connectivity, and integration with the Spotify API to fetch and display track information, playlists, and devices.
## Contributing
Contributions are welcome! Please fork the repository and submit a pull request with your changes.

656
Spotif.ino Normal file
View File

@@ -0,0 +1,656 @@
#include <LilyGo_AMOLED.h>
#include <LV_Helper.h>
#include <WiFiClientSecure.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <esp_wifi.h>
#include <time.h>
#include <vector>
#include "spotify_api.h"
#ifndef WIFI_SSID
#define WIFI_SSID "Menahem"
#endif
#ifndef WIFI_PASS
#define WIFI_PASS "203788583"
#endif
// Spotify API credentials
static String spotifyClientId = "2924245bc9054df49d32bf9f24079521";
static String spotifyClientSecret = "1c65fc2be3484a4ea9209b7fb498597b";
static String accessToken = "BQDU3_HI25XwIjmEDmthSFueTkXibjeKyuQcg9CE9v6C7HoKcso-7oQxnd_WTAXSLsdwvoo5FsSth95l3hvhxSw_6TKn-qk8zAIUDjcE9YffJASN-Y_03hGxCTzSwCpgEdvlX9ewilKlCkhL-GCydHjGugb5XgTBNGufRuSecahSQQkh4i0cram5uxCX1Co";
static String refreshToken = "AQDXArrdeyUcTJXApxOATdH8UixiJV38rmW9I6TNH1XAbaN4ZxbbwKwIzmkHQXrzswSVJX2D3RwB4X70V6OGUtz5L5WrPgCGS0e16FSStujYi5_Xve0RicCo8Q84UFLJdUE";
const char *spotifyBaseApiUrl = "https://api.spotify.com/v1";
const uint32_t C_SPOTIFY_GREEN = 0x1DB954;
const uint32_t C_SPOTIFY_BLACK = 0x000000;
const uint32_t C_WHITE = 0xFFFFFF;
// Variables to store current track information
int currentTrackDuration = 0;
int currentTrackProgress = 0;
bool isPlaying = false;
int httpErrorCount = 0;
const int maxErrorCount = 3;
String currentTrackImageUrl = "";
static unsigned long lastActivityTime = millis();
// LVGL objects for the display
lv_obj_t *labelTrack = nullptr;
lv_obj_t *labelArtist = nullptr;
lv_obj_t *barProgress = nullptr;
lv_obj_t *labelCurrentTime = nullptr;
lv_obj_t *labelTotalTime = nullptr;
lv_obj_t *btnContainer = nullptr;
lv_obj_t *btnPlayPause = nullptr;
lv_obj_t *btnNext = nullptr;
lv_obj_t *btnPrevious = nullptr;
lv_obj_t *btnVolumeDown = nullptr;
lv_obj_t *btnVolumeUp = nullptr;
lv_obj_t *labelNowPlaying = nullptr;
lv_obj_t *screen1 = nullptr;
lv_obj_t *screen2 = nullptr;
lv_obj_t *labelScreen2 = nullptr;
lv_obj_t *deviceList = nullptr;
lv_obj_t *splashScreen = nullptr;
lv_obj_t *imgTrack = nullptr;
lv_obj_t *labelUsername = nullptr;
lv_obj_t *labelBrowse = nullptr;
lv_obj_t *btnSettings = nullptr;
lv_obj_t *labelLoadStep = nullptr; // New label for load step
lv_obj_t *browseScreen = nullptr;
// Task handles for updating track information and displaying the current track
TaskHandle_t updateTrackInfoTaskHandle = NULL;
TaskHandle_t fetchTrackImageTaskHandle = NULL;
TaskHandle_t fetchPlaylistImageTaskHandle = NULL;
// Create an instance of the LilyGo_Class for the AMOLED display
LilyGo_Class amoled;
// Create an instance of Spotify API
SpotifyApi apiClient(spotifyClientId, spotifyClientSecret, accessToken, refreshToken);
// Function declarations
void createPlayingNowScreen();
void createSettingsScreen();
void createBrowseScreen();
void WiFiEvent(WiFiEvent_t event);
void updateTrackInfo(void *parameter);
void fetchTrackImage(void *parameter);
void fetchPlaylistImage(void *parameter);
void connectToWiFi();
void checkAndReconnectWiFi();
void dimScreen();
void wakeScreen();
// Helper function to create buttons
lv_obj_t *createButton(lv_obj_t *parent, const char *symbol, lv_event_cb_t event_cb, int width = 90, int height = 60, lv_color_t bg_color = lv_color_hex(0xFFFFFF)) {
lv_obj_t *btn = lv_btn_create(parent);
lv_obj_set_size(btn, width, height);
lv_obj_set_style_bg_color(btn, bg_color, 0);
lv_obj_t *label = lv_label_create(btn);
lv_label_set_text(label, symbol);
lv_obj_set_style_text_font(label, &lv_font_montserrat_28, 0);
if (bg_color.full == lv_color_hex(C_WHITE).full) {
lv_obj_set_style_text_color(label, lv_color_hex(C_SPOTIFY_BLACK), 0);
}
lv_obj_center(label);
lv_obj_add_event_cb(btn, event_cb, LV_EVENT_CLICKED, NULL);
return btn;
}
// Function to create the "Playing Now" screen
void createPlayingNowScreen() {
// Create screen1
screen1 = lv_obj_create(NULL);
lv_scr_load(screen1);
// Set the LVGL theme to dark
lv_theme_t *dark_theme = lv_theme_default_init(NULL, lv_palette_main(LV_PALETTE_GREY), lv_palette_main(LV_PALETTE_BLUE), true, LV_FONT_DEFAULT);
lv_disp_set_theme(NULL, dark_theme);
// Create LVGL objects for the display
lv_obj_t *topContainer = lv_obj_create(screen1);
lv_obj_set_width(topContainer, lv_obj_get_width(screen1));
lv_obj_align(topContainer, LV_ALIGN_TOP_MID, 0, 0); // Adjusted alignment to stick to the top
lv_obj_set_flex_flow(topContainer, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(topContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_bg_color(topContainer, lv_color_hex(C_SPOTIFY_GREEN), 0); // Set background color to the same as the play button
lv_obj_set_style_radius(topContainer, 0, 0); // Remove border radius
lv_obj_set_style_border_width(topContainer, 0, 0); // Remove border
lv_obj_set_height(topContainer, LV_SIZE_CONTENT); // Set height to fit content
labelUsername = lv_label_create(topContainer); // Changed parent to topContainer
lv_obj_set_style_text_font(labelUsername, &lv_font_montserrat_28, 0);
lv_label_set_text_fmt(labelUsername, "Hello, %s", apiClient.getUsername().c_str());
btnSettings = createButton(
topContainer, LV_SYMBOL_SETTINGS, [](lv_event_t *e) {
wakeScreen();
createSettingsScreen();
},
40, 40, lv_color_hex(C_WHITE));
imgTrack = lv_img_create(screen1);
lv_obj_set_size(imgTrack, 64, 64);
lv_obj_align(imgTrack, LV_ALIGN_TOP_LEFT, 10, 135); // Moved 10 px above
lv_obj_set_style_radius(imgTrack, LV_RADIUS_CIRCLE, 0); // Make it round
lv_obj_t *textContainer = lv_obj_create(screen1);
lv_obj_remove_style_all(textContainer);
lv_obj_set_width(textContainer, lv_obj_get_width(screen1) - 94);
lv_obj_align(textContainer, LV_ALIGN_TOP_LEFT, 84, 135); // Moved 10 px above
lv_obj_set_flex_flow(textContainer, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(textContainer, LV_FLEX_ALIGN_SPACE_AROUND, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_height(textContainer, LV_SIZE_CONTENT); // Set height to fit content
labelTrack = lv_label_create(textContainer);
lv_obj_set_style_text_font(labelTrack, &lv_font_montserrat_32, 0);
lv_label_set_long_mode(labelTrack, LV_LABEL_LONG_SCROLL_CIRCULAR); // Enable marquee effect for long text
labelArtist = lv_label_create(textContainer);
lv_obj_set_style_text_font(labelArtist, &lv_font_montserrat_22, 0); // Set height to fit content
lv_label_set_long_mode(labelArtist, LV_LABEL_LONG_WRAP); // Enable line wrapping
btnContainer = lv_obj_create(screen1);
lv_obj_set_size(btnContainer, lv_obj_get_width(screen1), 120);
lv_obj_align(btnContainer, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_set_flex_flow(btnContainer, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(btnContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
lv_obj_set_style_bg_color(btnContainer, lv_color_hex(C_SPOTIFY_BLACK), 0);
lv_obj_set_style_radius(btnContainer, 0, 0);
lv_obj_set_style_border_width(btnContainer, 0, 0); // Remove border
lv_obj_clear_flag(btnContainer, LV_OBJ_FLAG_SCROLLABLE); // Disable scrolling
btnVolumeDown = createButton(btnContainer, LV_SYMBOL_VOLUME_MID, [](lv_event_t *e) {
wakeScreen();
apiClient.setVolume(0);
});
btnPrevious = createButton(btnContainer, LV_SYMBOL_PREV, [](lv_event_t *e) {
wakeScreen();
apiClient.controlSpotify("previous");
});
btnPlayPause = createButton(
btnContainer, isPlaying ? LV_SYMBOL_PAUSE : LV_SYMBOL_PLAY, [](lv_event_t *e) {
wakeScreen();
apiClient.controlSpotify(isPlaying ? "pause" : "play");
},
110, 70, lv_color_hex(C_SPOTIFY_GREEN));
btnNext = createButton(btnContainer, LV_SYMBOL_NEXT, [](lv_event_t *e) {
wakeScreen();
apiClient.controlSpotify("next");
});
btnVolumeUp = createButton(btnContainer, LV_SYMBOL_VOLUME_MAX, [](lv_event_t *e) {
wakeScreen();
apiClient.setVolume(100);
});
barProgress = lv_bar_create(screen1);
lv_obj_set_size(barProgress, lv_obj_get_width(screen1) - 20, 20);
lv_obj_align(barProgress, LV_ALIGN_BOTTOM_MID, 0, -140); // Moved 30 px above
lv_obj_t *timeContainer = lv_obj_create(screen1);
lv_obj_remove_style_all(timeContainer);
lv_obj_set_size(timeContainer, lv_obj_get_width(screen1) - 20, 40);
lv_obj_align_to(timeContainer, barProgress, LV_ALIGN_OUT_TOP_MID, 0, 0);
lv_obj_set_flex_flow(timeContainer, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(timeContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
labelCurrentTime = lv_label_create(timeContainer);
lv_obj_set_style_text_font(labelCurrentTime, &lv_font_montserrat_24, 0);
labelTotalTime = lv_label_create(timeContainer);
lv_obj_set_style_text_font(labelTotalTime, &lv_font_montserrat_24, 0);
// Create a task to update track information
if (!updateTrackInfoTaskHandle) {
xTaskCreate(updateTrackInfo, "UpdateTrackInfo", 5 * 1024, NULL, 12, &updateTrackInfoTaskHandle);
}
// Add swipe gesture to switch between screens
lv_obj_add_event_cb(
screen1, [](lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_GESTURE) {
lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act());
if (dir == LV_DIR_LEFT) {
createSettingsScreen();
} else if (dir == LV_DIR_TOP) {
createBrowseScreen();
}
}
},
LV_EVENT_GESTURE, NULL);
// Add touch event to wake screen
lv_obj_add_event_cb(
screen1, [](lv_event_t *e) {
wakeScreen();
},
LV_EVENT_CLICKED, NULL);
}
void createBrowseScreen() {
browseScreen = lv_obj_create(NULL);
lv_scr_load(browseScreen);
// Create LVGL objects for the display
lv_obj_t *topBrowseContainer = lv_obj_create(browseScreen);
lv_obj_set_width(topBrowseContainer, lv_obj_get_width(screen1));
lv_obj_align(topBrowseContainer, LV_ALIGN_TOP_MID, 0, 0); // Adjusted alignment to stick to the top
lv_obj_set_flex_flow(topBrowseContainer, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(topBrowseContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_bg_color(topBrowseContainer, lv_color_hex(C_SPOTIFY_GREEN), 0); // Set background color to the same as the play button
lv_obj_set_style_radius(topBrowseContainer, 0, 0); // Remove border radius
lv_obj_set_style_border_width(topBrowseContainer, 0, 0); // Remove border
lv_obj_set_height(topBrowseContainer, LV_SIZE_CONTENT); // Set height to fit content
labelBrowse = lv_label_create(topBrowseContainer);
lv_label_set_text(labelBrowse, "Browse");
lv_obj_set_style_text_font(labelBrowse, &lv_font_montserrat_28, 0);
// Create a list to display featured playlists
lv_obj_t *playlistList = lv_list_create(browseScreen);
lv_obj_set_size(playlistList, lv_obj_get_width(browseScreen) - 20, lv_obj_get_height(browseScreen) - 75);
lv_obj_align(playlistList, LV_ALIGN_TOP_LEFT, 10, 75);
// Fetch featured playlists from the API client
auto [playlists, playlistCount] = apiClient.getFeaturedPlaylists(5, 0);
checkAndReconnectWiFi();
if (playlists == nullptr || playlistCount == 0) {
Serial.println("No playlists found or failed to fetch playlists.");
return;
}
// Add playlists to the list
for (size_t i = 0; i < playlistCount; ++i) {
lv_obj_t *list_btn = lv_list_add_btn(playlistList, NULL, playlists[i].name.c_str());
char *playlistId = new char[playlists[i].id.length() + 1];
strcpy(playlistId, playlists[i].id.c_str());
lv_obj_set_user_data(list_btn, (void *)playlistId); // Use actual id as user data
// Create an image object for the playlist cover
lv_obj_t *img = lv_img_create(list_btn);
lv_obj_set_size(img, 64, 64);
lv_obj_align(img, LV_ALIGN_LEFT_MID, 10, 0);
// Fetch the image from the remote URL in a non-blocking task
if (!fetchPlaylistImageTaskHandle) {
xTaskCreate(fetchPlaylistImage, "FetchPlaylistImage", 5 * 1024, (void *)img, 12, &fetchPlaylistImageTaskHandle);
}
lv_obj_add_event_cb(
list_btn, [](lv_event_t *e) {
lv_obj_t *btn = lv_event_get_target(e);
const char *playlistId = (const char *)lv_obj_get_user_data(btn);
apiClient.playPlaylist(String(playlistId));
checkAndReconnectWiFi();
Serial.println("Selected playlist ID: " + String(playlistId));
lv_scr_load(screen1); // Move back to playing now screen
},
LV_EVENT_CLICKED, NULL);
}
// Clean up allocated memory for playlists
delete[] playlists;
// Add swipe gesture to switch between screens
lv_obj_add_event_cb(
browseScreen, [](lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_GESTURE) {
lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act());
if (dir == LV_DIR_BOTTOM) {
lv_scr_load(screen1); // Move back to playing now screen
}
}
},
LV_EVENT_GESTURE, NULL);
// Add touch event to wake screen
lv_obj_add_event_cb(
browseScreen, [](lv_event_t *e) {
wakeScreen();
},
LV_EVENT_CLICKED, NULL);
}
// Function to create the settings screen
void createSettingsScreen() {
// Create screen2
screen2 = lv_obj_create(NULL);
lv_scr_load(screen2); // Ensure the screen is loaded before adding objects
lv_obj_t *topDevicesContainer = lv_obj_create(screen2);
lv_obj_set_width(topDevicesContainer, lv_obj_get_width(screen1));
lv_obj_align(topDevicesContainer, LV_ALIGN_TOP_MID, 0, 0); // Adjusted alignment to stick to the top
lv_obj_set_flex_flow(topDevicesContainer, LV_FLEX_FLOW_ROW);
lv_obj_set_flex_align(topDevicesContainer, LV_FLEX_ALIGN_SPACE_BETWEEN, LV_FLEX_ALIGN_START, LV_FLEX_ALIGN_START);
lv_obj_set_style_bg_color(topDevicesContainer, lv_color_hex(C_SPOTIFY_GREEN), 0); // Set background color to the same as the play button
lv_obj_set_style_radius(topDevicesContainer, 0, 0); // Remove border radius
lv_obj_set_style_border_width(topDevicesContainer, 0, 0); // Remove border
lv_obj_set_height(topDevicesContainer, LV_SIZE_CONTENT); // Set height to fit content
labelScreen2 = lv_label_create(topDevicesContainer);
lv_label_set_text(labelScreen2, "Available Devices:");
lv_obj_set_style_text_font(labelScreen2, &lv_font_montserrat_28, 0);
deviceList = lv_list_create(screen2);
if (deviceList == nullptr) {
Serial.println("Failed to create device list");
return;
}
lv_obj_set_size(deviceList, lv_obj_get_width(screen2) - 20, lv_obj_get_height(screen2) - 75);
lv_obj_align(deviceList, LV_ALIGN_TOP_LEFT, 10, 75);
Serial.println("Fetching devices list...");
// Fetch devices from the API client
auto [devices, deviceCount] = apiClient.getDevicesList();
checkAndReconnectWiFi();
if (devices == nullptr || deviceCount == 0) {
Serial.println("No devices found or failed to fetch devices.");
return;
}
// Add devices to the list
for (size_t i = 0; i < deviceCount; ++i) {
lv_obj_t *list_btn = lv_list_add_btn(deviceList, LV_SYMBOL_AUDIO, devices[i].name.c_str());
char *deviceId = new char[devices[i].id.length() + 1];
strcpy(deviceId, devices[i].id.c_str());
lv_obj_set_user_data(list_btn, (void *)deviceId); // Use actual id as user data
lv_obj_add_event_cb(
list_btn, [](lv_event_t *e) {
lv_obj_t *btn = lv_event_get_target(e);
const char *deviceId = (const char *)lv_obj_get_user_data(btn);
apiClient.setActiveDevice(String(deviceId));
checkAndReconnectWiFi();
Serial.println("Selected device ID: " + String(deviceId));
},
LV_EVENT_CLICKED, NULL);
}
// Clean up allocated memory for devices
delete[] devices;
// Add swipe gesture to switch between screens
lv_obj_add_event_cb(
screen2, [](lv_event_t *e) {
lv_event_code_t code = lv_event_get_code(e);
if (code == LV_EVENT_GESTURE) {
lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act());
if (dir == LV_DIR_RIGHT) {
lv_scr_load(screen1);
}
}
},
LV_EVENT_GESTURE, NULL);
// Add touch event to wake screen
lv_obj_add_event_cb(
screen2, [](lv_event_t *e) {
wakeScreen();
},
LV_EVENT_CLICKED, NULL);
}
// Function to show the splash screen
void showSplashScreen() {
splashScreen = lv_obj_create(NULL);
lv_obj_set_style_bg_color(splashScreen, lv_color_hex(C_SPOTIFY_GREEN), 0);
lv_obj_t *label = lv_label_create(splashScreen);
lv_label_set_text(label, "Spotify Remote Controller");
lv_obj_set_style_text_color(label, lv_color_hex(C_WHITE), 0);
lv_obj_set_style_text_font(label, &lv_font_montserrat_32, 0);
lv_obj_center(label);
// Create and position the load step label
labelLoadStep = lv_label_create(splashScreen);
lv_label_set_text(labelLoadStep, "Loading...");
lv_obj_set_style_text_color(labelLoadStep, lv_color_hex(C_WHITE), 0);
lv_obj_set_style_text_font(labelLoadStep, &lv_font_montserrat_24, 0);
lv_obj_align(labelLoadStep, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_scr_load(splashScreen);
lv_task_handler(); // Ensure the splash screen is rendered
}
// Function to set up the device
void setup() {
Serial.begin(115200);
Serial.println("============================================");
Serial.println("Welcome to spotify player");
Serial.println("============================================");
// Initialize WiFi in station mode
WiFi.mode(WIFI_STA);
// Initialize the AMOLED display
bool rslt = false;
rslt = amoled.beginAMOLED_241();
if (!rslt) {
while (1) {
Serial.println("The board model cannot be detected, please raise the Core Debug Level to an error");
delay(500);
}
}
// Initialize LVGL helper
beginLvglHelper(amoled);
showSplashScreen();
// Connect to WiFi
connectToWiFi();
lastActivityTime = millis();
createPlayingNowScreen();
}
void updateLoadStep(const char *step) {
if (labelLoadStep) {
lv_label_set_text(labelLoadStep, step);
lv_task_handler();
}
}
// Function to handle the main loop
void loop() {
// Handle LVGL tasks
lv_task_handler();
delay(1);
// Check for inactivity and dim screen if needed
if (millis() - lastActivityTime > 20000) {
dimScreen();
}
}
// Function to handle WiFi events
void WiFiEvent(WiFiEvent_t event) {
switch (event) {
case ARDUINO_EVENT_WIFI_SCAN_DONE:
Serial.println("Completed scan for access points");
break;
case ARDUINO_EVENT_WIFI_STA_START:
Serial.println("WiFi client started");
break;
case ARDUINO_EVENT_WIFI_STA_STOP:
Serial.println("WiFi client stopped");
break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
Serial.println("Connected to access point");
break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
Serial.println("Disconnected from WiFi access point");
break;
case ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE:
Serial.println("Authentication mode of access point has changed");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
Serial.print("Obtained IP address: ");
Serial.println(WiFi.localIP());
if (!updateTrackInfoTaskHandle) {
xTaskCreate(updateTrackInfo, "UpdateTrackInfo", 5 * 1024, NULL, 12, &updateTrackInfoTaskHandle);
}
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
Serial.println("Lost IP address and IP address is reset to 0");
break;
default:
break;
}
}
// Function to update track information
void updateTrackInfo(void *parameter) {
for (;;) {
TrackInfo trackInfo = apiClient.getCurrentTrackInfo();
checkAndReconnectWiFi();
isPlaying = trackInfo.isPlaying;
lv_label_set_text(labelTrack, trackInfo.name.c_str());
lv_label_set_text(labelArtist, trackInfo.artistName.c_str());
lv_label_set_text(lv_obj_get_child(btnPlayPause, NULL), trackInfo.isPlaying ? LV_SYMBOL_PAUSE : LV_SYMBOL_PLAY);
if (trackInfo.coverUrl != currentTrackImageUrl) {
currentTrackImageUrl = trackInfo.coverUrl;
if (!fetchTrackImageTaskHandle) {
xTaskCreate(fetchTrackImage, "FetchTrackImage", 5 * 1024, (void *)currentTrackImageUrl.c_str(), 12, &fetchTrackImageTaskHandle);
}
}
currentTrackDuration = trackInfo.duration;
currentTrackProgress = trackInfo.progress;
if (currentTrackDuration != 0) {
lv_bar_set_value(barProgress, (currentTrackProgress * 100) / currentTrackDuration, LV_ANIM_OFF);
}
int currentTimeSec = currentTrackProgress / 1000;
int totalTimeSec = currentTrackDuration / 1000;
char currentTimeStr[10];
char totalTimeStr[10];
snprintf(currentTimeStr, sizeof(currentTimeStr), "%02d:%02d", currentTimeSec / 60, currentTimeSec % 60);
snprintf(totalTimeStr, sizeof(totalTimeStr), "%02d:%02d", totalTimeSec / 60, totalTimeSec % 60);
lv_label_set_text(labelCurrentTime, currentTimeStr);
lv_label_set_text(labelTotalTime, totalTimeStr);
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
// Function to fetch track image
void fetchTrackImage(void *parameter) {
const char *imageUrl = (const char *)parameter;
HTTPClient http;
http.begin(imageUrl);
int httpResponseCode = http.GET();
if (httpResponseCode == HTTP_CODE_OK) {
int len = http.getSize();
uint8_t *buffer = (uint8_t *)malloc(len);
if (buffer) {
WiFiClient *stream = http.getStreamPtr();
stream->readBytes(buffer, len);
// Save the original image in ROM
lv_img_dsc_t *img_dsc = (lv_img_dsc_t *)malloc(sizeof(lv_img_dsc_t));
if (img_dsc) {
img_dsc->data = buffer;
img_dsc->data_size = len;
img_dsc->header.always_zero = 0;
img_dsc->header.w = 64;
img_dsc->header.h = 64;
img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR;
lv_img_set_src(imgTrack, img_dsc);
} else {
free(buffer);
}
}
} else {
Serial.println("Failed to fetch track image, error: " + String(httpResponseCode));
if (httpResponseCode == -1) {
Serial.println("HTTP request failed with error: -1, reconnecting WiFi...");
connectToWiFi();
}
}
http.end();
fetchTrackImageTaskHandle = NULL;
vTaskDelete(NULL);
}
// Function to fetch playlist image
void fetchPlaylistImage(void *parameter) {
lv_obj_t *img = (lv_obj_t *)parameter;
const char *imageUrl = (const char *)lv_obj_get_user_data(img);
HTTPClient http;
http.begin(imageUrl);
int httpResponseCode = http.GET();
if (httpResponseCode == HTTP_CODE_OK) {
int len = http.getSize();
uint8_t *buffer = (uint8_t *)malloc(len);
if (buffer) {
WiFiClient *stream = http.getStreamPtr();
stream->readBytes(buffer, len);
// Save the original image in ROM
lv_img_dsc_t *img_dsc = (lv_img_dsc_t *)malloc(sizeof(lv_img_dsc_t));
if (img_dsc) {
img_dsc->data = buffer;
img_dsc->data_size = len;
img_dsc->header.always_zero = 0;
img_dsc->header.w = 64;
img_dsc->header.h = 64;
img_dsc->header.cf = LV_IMG_CF_TRUE_COLOR;
lv_img_set_src(img, img_dsc);
} else {
free(buffer);
}
}
} else {
Serial.println("Failed to fetch playlist image, error: " + String(httpResponseCode));
if (httpResponseCode == -1) {
Serial.println("HTTP request failed with error: -1, reconnecting WiFi...");
connectToWiFi();
}
}
http.end();
fetchPlaylistImageTaskHandle = NULL;
vTaskDelete(NULL);
}
// Function to connect to WiFi
void connectToWiFi() {
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) {
updateLoadStep("Connecting to WiFi...");
delay(1000);
}
updateLoadStep("Connected to WiFi");
delay(500);
}
// Function to check and reconnect WiFi if needed
void checkAndReconnectWiFi() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected, reconnecting...");
connectToWiFi();
}
}
// Function to dim the screen
void dimScreen() {
amoled.setBrightness(15);
}
// Function to wake the screen
void wakeScreen() {
amoled.setBrightness(100);
lastActivityTime = millis(); // Reset inactivity timer
}

483
spotify_api.cpp Normal file
View File

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

50
spotify_api.h Normal file
View File

@@ -0,0 +1,50 @@
#pragma once
struct TrackInfo
{
String name;
String artistName;
String coverUrl;
int duration;
int progress;
bool isPlaying;
};
struct FeaturedPlaylist
{
String name;
String id;
String imageUrl;
};
struct Device
{
String id;
String name;
};
class SpotifyApi
{
public:
int errorCount;
int lastHttpResponseCode;
SpotifyApi(String, String, String, String);
TrackInfo getCurrentTrackInfo();
String refreshAccessToken();
std::tuple<Device *, size_t> getDevicesList();
void setActiveDevice(String);
String getUsername();
bool performHttpRequestWithRetry(HTTPClient &http, String &apiUrl, String &response);
void controlSpotify(String command);
void setVolume(int volume);
void playPlaylist(String playlistId);
std::tuple<FeaturedPlaylist *, size_t> getFeaturedPlaylists(int limit, int offset);
std::tuple<FeaturedPlaylist *, size_t> getUserPlaylists(int limit, int offset);
private:
String clientId;
String clientSecret;
String accessToken;
String refreshToken;
void withAuth(HTTPClient &http);
};