Controlling music seamlessly while maintaining aesthetics and functionality can be a challenge—especially when using compact keyboards. This inspired me to create a custom Spotify Macro Keyboard that combines functionality, dynamic lighting, and modern design principles.
With satisfying clicky mechanical switches, vibrant RGB lighting, and a smart OLED display, this keyboard became an essential quality-of-life upgrade for controlling my music.
The Spotify Macro Keyboard is a compact and modern custom macro keyboard equipped with features such as:
This project combined hardware design, coding, and API integration to bring together a functional and aesthetically pleasing device.
There were many things I had to research before creating this Spotify Macro Keyboard. I started by considering the base functions I wanted my keyboard to have:
Initially, I looked at RGB LEDs available to me, but they required three separate pins plus a ground pin. While they could display colors, controlling them precisely and fitting them neatly into a case was challenging.
After further research, I discovered the WS2812B addressable LED strips. These LEDs:
I initially considered Liquid Crystal Displays (LCDs), which we used in labs. However, they required up to 14 pins, which was inefficient given the number of components I needed to connect.
I found a better alternative in the SSD1306 0.96-inch OLED display, which had:
This choice introduced me to the I2C protocol, a highly efficient way to connect multiple devices using minimal pins.
For a satisfying tactile experience, I selected blue mechanical switches. These switches:
I kept the design straightforward to ensure optimal alignment when integrating switches into the custom PCB.
The biggest challenge was determining the album art color for dynamic lighting. Processing images on an Arduino R3 wasn’t feasible because:
160x160x3 = 76,800 bytes
or ~75 KB.To address these limitations, I chose the ESP32 Dev Module 1, which offered:
However, even the ESP32 struggled to process album images directly due to storage constraints. Moving large amounts of data on such a small processor would slow down operations.
To overcome this, I built a REST API that offloaded heavy computation to a server:
This solution not only solved the memory issue but also provided flexibility to add new features later.
I designed a custom PCB using EasyEDA, starting with basic layouts and gradually refining the design. Here’s was my general process:
Unfortunately, I made a crucial error here, making use of a pin that could not be used as a standard GPIO pin under certain conditions and so I had to go back and quickly revise this final design one last time.
The final PCB looked pretty good in my opinion, the many different colours of the outlines represented the layer on which each component would be. This is similar to how photoshop works, here, pink was the outline, yellow was the silk layer, and red and blue were the first and second layer respectively.
The final design could then be viewed in 3D and I had it printed and shipped over!
My code for this project was decently long and so to describe and explain the general idea, I've split it into 3 distinct parts.
I started by defining all constants, credentials, and bitmaps in a separate file. This file contained:
1const String PASSWORD = "REDACTED";2const char SSID[] = "REDACTED";3const char SSID_PASS[] = "REDACTED";4const char *benzServerCert =5"-----BEGIN CERTIFICATE-----\n"6"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"7// This part keeps on going for a while8"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"9"-----END CERTIFICATE-----";10#define RGB_PIN 1811// ... This also goes on for a while12#define SDA 2113#define SCREEN_WIDTH 12814#define SCREEN_HEIGHT 6415#define OLED_RESET -116const unsigned char wifi_2[] PROGMEM = {170xff, 0xff, 0xff, 0xff, 0xf0, 0x0f, 0xc1, 0x83, 0x9f, 0xf1, 0x38,180x1c, 0xe1, 0x87, 0xcf, 0xf3, 0xd8, 0x1b, 0xf1, 0x8f, 0xf3, 0xcf,190xfe, 0x7f, 0xfc, 0x3f, 0xfe, 0x7f, 0xff, 0xff, 0xff, 0xff};
The main program handled button inputs, display updates, and API requests. It included:
But to start, I simply imported all necessary libraries, which included:
1#include <Adafruit_GFX.h>2#include <Adafruit_SSD1306.h>3#include <FastLED.h>4#include <SMBCredentials.h>5#include <WiFi.h>6#include <WiFiClientSecure.h>7#include <Wire.h>
Next I defined many variables and pieces of data that I would need access to later.
1WiFiClientSecure wifiClient;2TwoWire I2C = TwoWire(0);3Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &I2C, OLED_RESET);4const int keys[7] = {SHUFFLE, VOLUME_DEC, VOLUME_INC, LOOP,5BACK, PAUSE_PLAY, SKIP};6bool keyPrevState[7] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};7bool keyState[7] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};8const unsigned long RSSIDelay = 10000;9const unsigned long currentDelay = 5000;10const unsigned long timeDelay = 1000;11const unsigned long fadeDelay = 4;12unsigned long nextRSSICheck, nextCurrentCheck, nextTimeCheck, nextFade;13String title, artist, album, color, durationRaw, progressRaw, pausedRaw,14volumeRaw, lastTitle;15int progress, duration, volume, lastVolume;16bool paused;17int rssi = 0;18CRGB LEDs[RGB_LED_NUM];19int red, blue;20int green = 255;21bool shouldFade = false;22int prevRed, prevGreen, prevBlue;23int step;
Then, for each pin that I had defined earlier, I set them up as inputs or outputs, I set some pins to pull up to avoid floating values. I also made sure to setup the LED strip and the OLED display.
I added a delay to allow the display to initialize and then I checked if the display was initialized correctly, if not, I set all the LEDs to a bright orange and then entered an infinite loop to show that something was wrong.
1// The setup function contents2for (int i = 0; i < 7; i++) {3pinMode(keys[i], INPUT_PULLUP);4}5pinMode(LED, OUTPUT);6pinMode(SCL, INPUT_PULLUP);7pinMode(SDA, INPUT_PULLUP);8delay(2000);9FastLED.addLeds<CHIP_SET, LED, COLOR_CODE>(LEDs, RGB_LED_NUM);10FastLED.setBrightness(BRIGHTNESS);11FastLED.setMaxPowerInVoltsAndMilliamps(5, 500);12for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::White;13FastLED.show();14I2C.begin(SDA, SCL, 400000);15if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {16for (int i = 0; i < RGB_LED_NUM; i++) {17LEDs[i] = CRGB::OrangeRed;18}19FastLED.show();20for (;;) {21;22}23}24delay(2000);25display.clearDisplay();
I then finally moved onto connecting to the WiFi network, this process does take a bit of time and doesn't always work and so I made a loop to try and connect to the network 20 times before giving up and showing a red LED. During this time, it would also show a breathing effect on the LEDs to indicate it was attempting a connection. Once connected to the network, I would then fade the LEDs from white to green to indicate a successful connection.
To ensure a smooth transition, I modelled a sinusoidal wave in the form of y = asin(bx + c) + d, where a, b, c, and d are constants. This allowed me to smoothly transition between colors with a being the amplitude (110), b being the frequency, c being the phase shift (none), and d being the vertical shift (140). Calculating the frequency was less of a concern so I just ballparked that.
1WiFi.mode(WIFI_STA);2WiFi.begin(SSID, SSID_PASS);3while (WiFi.status() != WL_CONNECTED) {4for (int times = 0; times < 20; times++) {5for (int i = 0; i < RGB_LED_NUM; i++) {6byte brightness = 140 + 110 * sin(millis() / 250.0);7LEDs[i] = CRGB::White;8LEDs[i].fadeToBlackBy(255 - brightness);9}10delay(25);11FastLED.show();12}13}14for (int step = 0; step < 256; step++) {15for (int i = 0; i < RGB_LED_NUM; i++) {16LEDs[i] = blend(CRGB::White, CRGB::Green, step);17}18delay(3);19FastLED.show();20}21wifiClient.setCACert(benzServerCert);
This function was simply meant to update the display with the current song, artist, and progress. It would also update the progress bar and the volume icon. This took a lot of trial and error but eventually, I got it to work. I also got to learn a bit about RSSI and how to use it to determine the strength of the WiFi signal.
1display.setCursor(0, 0);2display.setTextColor(WHITE);3display.setTextWrap(false);4display.setTextSize(2);5for (int i = 0; i < 2; i++) {6for (int j = 0; j < 16; j++) {7display.drawPixel(i, j, WHITE);8}9}10for (int i = 2; i < 18; i++) {11for (int j = 0; j < 16; j++) {12display.drawPixel(i, j, BLACK);13}14}15for (int i = 18; i < 64; i++) {16for (int j = 0; j < 16; j++) {17display.drawPixel(i, j, WHITE);18}19}20if (rssi == 0) {21display.drawBitmap(2, 0, wifi_0, 16, 16, WHITE);22} else if (rssi < -70) {23display.drawBitmap(2, 0, wifi_1, 16, 16, WHITE);24} else {25display.drawBitmap(2, 0, wifi_2, 16, 16, WHITE);26}27for (int i = 64; i < 108; i++) {28for (int j = 0; j < 16; j++) {29display.drawPixel(i, j, WHITE);30}31}32for (int i = 108; i < 124; i++) {33for (int j = 0; j < 16; j++) {34display.drawPixel(i, j, BLACK);35}36}37if (volume > 66) {38display.drawBitmap(108, 0, volume_3, 16, 16, WHITE);39} else if (volume > 33) {40display.drawBitmap(108, 0, volume_2, 16, 16, WHITE);41} else if (volume > 0) {42display.drawBitmap(108, 0, volume_1, 16, 16, WHITE);43} else {44display.drawBitmap(108, 0, volume_0, 16, 16, WHITE);45}46for (int i = 124; i < 128; i++) {47for (int j = 0; j < 16; j++) {48display.drawPixel(i, j, WHITE);49}50}51display.display();
This function is a bit more complex but its simply meant to extract a value from a JSON string given a key. I had to make this since other libraries accounted for a lot of things I wouldn't need to, and they also took up too much memory, and runtime.
1void extractValue(const String& key, const String& json, String& result) {2String quoteKey = "\"" + key + "\":";3int start = json.indexOf(quoteKey);4if (start != -1) {5int end = json.indexOf(",", start + quoteKey.length());6if (end == -1) {7end = json.indexOf("}", start + quoteKey.length());8}9result = json.substring(start + quoteKey.length(), end);10result.trim();11result.remove(0, 1);12result.remove(result.length() - 1);13}14}
This function is meant to update the state of the music player, it would send a GET request to the server with the action and subaction, if any. It would then wait for a response and then update the state of the music player accordingly.
I made this go through my own server so I don't have to create another connection to Spotify's servers while also only making use of GET requests to keep things simple (the routes usually require GET, PUT, or POST).
1void updateState(char action, int subAction = 0) {2String actionString = "";3if (action == 'p') {4actionString = "playPause";5} else if (action == 'b') {6actionString = "back";7} else if (action == 's') {8actionString = "skip";9} else if (action == 'r') {10actionString = "loop";11} else if (action == 'v') {12if (subAction == 0) {13actionString = "vdec";14} else {15actionString = "vinc";16}17} else if (action == 'l') {18actionString = "loop";19} else if (action == 'f') {20actionString = "shuffle";21}22if (!wifiClient.connected()) {23if (!wifiClient.connect("benzhou.tech", 443)) {24for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::Red;25FastLED.show();26return;27}28yield();29}30wifiClient.print("GET /api/manageState/" + PASSWORD + "/" + actionString +31" HTTP/1.1\r\n" + "Host: benzhou.tech\r\n" +32"Connection: Keep-Alive\r\n\r\n");33wifiClient.flush();34}
This function is meant to ensure the current connection is being maintained and if not, attempt to reconnect. I made sure to include an error state where the LEDs would turn yellow if the connection was lost.
Afterwards, I would send a GET request to the server to get the current song and its details. I would then parse the JSON data and update the display accordingly.
The final part of this function is simply checking if the new song is different from the last song, if it is, then I would update the display with the new song and artist.
It is quite long and tedious, but simple in principle.
1if (!wifiClient.connected()) {2if (!wifiClient.connect("benzhou.tech", 443)) {3for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::Red;4FastLED.show();5return;6}7yield();8}9wifiClient.print("GET /api/getCurrent/" + PASSWORD + " HTTP/1.1\r\n" +10"Host: benzhou.tech\r\n" +11"Connection: Keep-Alive\r\n\r\n");12unsigned long timeout = millis();13while (!wifiClient.available()) {14if (millis() - timeout > 5000) {15for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::Yellow;16FastLED.show();17return;18}19}20yield();21char endOfHeaders[] = "\r\n\r\n";22if (!wifiClient.find(endOfHeaders)) {23for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::Orange;24FastLED.show();25return;26}27wifiClient.find("{\"ti");28String response = "{\"ti";29while (wifiClient.available()) {30char c = wifiClient.read();31response += c;32}33wifiClient.flush();34lastTitle = title;35extractValue("title", response, title);36extractValue("artist", response, artist);37extractValue("album", response, album);38extractValue("duration", response, durationRaw);39extractValue("progress", response, progressRaw);40extractValue("paused", response, pausedRaw);41extractValue("volume", response, volumeRaw);42paused = pausedRaw == "true";43lastVolume = volume;44volume = volumeRaw.toInt();45duration = durationRaw.toInt();46progress = progressRaw.toInt();47if (title != lastTitle) {48color = response.substring(response.indexOf("[") + 1,49response.indexOf("]"));50int comma1 = color.indexOf(",");51int comma2 = color.indexOf(",", comma1 + 1);52shouldFade = true;53prevRed = red;54prevGreen = green;55prevBlue = blue;56red = color.substring(0, comma1 + 1).toInt();57green = color.substring(comma1 + 1, comma2 + 1).toInt();58blue = color.substring(comma2 + 1, color.length()).toInt();59display.setTextColor(WHITE);60display.setTextWrap(false);61display.clearDisplay();62updateScreen();63display.setCursor(0, 16);64display.setTextSize(2);65display.println(title);66display.setTextSize(1);67display.println(artist);68}69display.display();
This function is meant to fade the LEDs from one color to another, it would do this by calculating the difference between the current color and the target color and then incrementing or decrementing the current color by a small amount.
1bool fadeLED() {2if (step > 255) {3step = 0;4return false;5}6for (int i = 0; i < RGB_LED_NUM; i++) {7LEDs[i] = blend(CRGB(prevRed, prevGreen, prevBlue),8CRGB(red, green, blue), step);9}10FastLED.show();11step += 1;12return true;13}
This function is meant to update the progress bar on the display, it would do this by calculating the percentage of the song that has been played and then updating the progress bar accordingly. It's also a simple function in principle, though it took a bit of time to get the correct logic and math.
1void updateTime(int total, int current) {2display.fillRect(0, 48, 128, 16, BLACK);3if (current > total) {4current = total;5}6int barWidth = 100;7int barHeight = 6;8int barX = 14;9int barY = 56;10display.drawRect(barX, barY, barWidth, barHeight, WHITE);11if (total <= 0) {12total = 1;13}14int percent = (100 * current) / total;15int progressWidth = (barWidth * percent) / 100;16display.fillRect(barX, barY, progressWidth, barHeight, WHITE);17int currentMinutes = current / 60;18int currentSeconds = current % 60;19int totalMinutes = total / 60;20int totalSeconds = total % 60;21display.setTextSize(1);22display.setCursor(32, 48);23display.print(currentMinutes);24display.print(":");25if (currentSeconds < 10) display.print("0");26display.print(currentSeconds);27display.print("/");28display.print(totalMinutes);29display.print(":");30if (totalSeconds < 10) display.print("0");31display.println(totalSeconds);32display.display();33}
This part was also quite tedious, but simply put, if we define a function callable for each button, we can then call the updateState function with the appropriate action and subaction.
1void shuffle() { updateState('f'); }2void volumeDecrease() {3updateState('v', 0);4updateScreen();5}6void volumeIncrease() {7updateState('v', 1);8updateScreen();9}10void repeat() { updateState('r'); }11void back() {12updateState('b');13nextCurrentCheck = millis() + 100;14}15void pausePlay() {16updateState('p');17nextCurrentCheck = millis() + 100;18}19void skip() {20updateState('s');21nextCurrentCheck = millis() + 100;22updateCurrent();23}24void (*funcs[7])() = {25shuffle, volumeDecrease, volumeIncrease, repeat, back, pausePlay, skip};
This on its own doesn't actually do anything but thats where the magical loop function comes in.
The loop function continuously runs for the entirety of the program, it checks for button presses and then calls the appropriate function. It also checks if the current song has changed and if so, updates the display accordingly as well as a bunch of other small checks.
1for (int i = 0; i < 7; i++) {2keyState[i] = digitalRead(keys[i]);3if (keyState[i] == LOW && keyPrevState[i] == HIGH) {4funcs[i]();5}6keyPrevState[i] = keyState[i];7}8if (millis() > nextRSSICheck) {9nextRSSICheck = millis() + RSSIDelay;10rssi = WiFi.RSSI();11updateScreen();12}13if (millis() > nextCurrentCheck) {14nextCurrentCheck = millis() + currentDelay;15updateCurrent();16}17if (millis() > nextTimeCheck) {18nextTimeCheck = millis() + timeDelay;19updateTime(duration, progress);20if (!paused) {21progress++;22}23}24if (shouldFade && millis() > nextFade) {25nextFade = millis() + fadeDelay;26shouldFade = fadeLED();27}
The ESP32 alone couldn't handle image processing for album art. To solve this, I developed a custom REST API hosted on my server. The API:
This approach offloaded heavy computation to a more capable server, allowing the ESP32 to focus on display and LED control.
My API itself can also be split into 3 separate routes along with a utility route.
This route is meant to get the dominant color of the album art, it would do this by fetching the album art from Spotify's servers and then processing it to get the dominant color.
1import type { NextApiRequest, NextApiResponse } from "next";2import { getColorFromURL } from "color-thief-node";34type Data = {5answer: number[];6};78function findNearestColor(rgbArray: number[]): number[] {9const colors: number[][] = [10[255, 0, 0],11[255, 125, 0],12[255, 255, 0],13[125, 255, 0],14[0, 255, 0],15[0, 255, 125],16[0, 255, 255],17[0, 125, 255],18[0, 0, 255],19[125, 0, 255],20[255, 0, 255],21[255, 0, 125],22];2324let minDistance: number = Infinity;25let closestColor: number[] = [];26colors.forEach((color: number[]) => {27const distance: number = Math.sqrt(28Math.pow(rgbArray[0] - color[0], 2) +29Math.pow(rgbArray[1] - color[1], 2) +30Math.pow(rgbArray[2] - color[2], 2)31);32if (distance < minDistance) {33minDistance = distance;34closestColor = color;35}36});37return closestColor;38}3940export default async function handler(41req: NextApiRequest,42res: NextApiResponse<Data>43) {44const hash = req.query.hash as string;45const url = `https://i.scdn.co/image/${hash}`;46let colors = await getColorFromURL(url);47const color = findNearestColor(colors);48res.status(200).json({ answer: color });49}
This accomplishes with a simple formula of root(r^2 + g^2 + b^2) to get the distance between two colors, once this formula is applied to all colors, the closest clean color is returned.
This route is meant to get the current song and its details, it would do this by fetching the current song from Spotify's servers and then parsing the JSON data to get the song, artist, album, duration, progress, paused, and volume.
1import { NextApiRequest, NextApiResponse } from "next";2import getSpotifyAccessToken from "@/utils/functions/getSpotify";3type ESPInfo = {4title: string;5artist: string;6album: string;7color: [number, number, number];8duration: string;9progress: string;10paused: string;11volume: string;12shuffle: boolean;13loop: string;14}15type Error = {16error: string;17}18export default async function handler(19req: NextApiRequest,20res: NextApiResponse<ESPInfo | Error>21) {22const { password } = req.query;23if (password !== process.env.PASSWORD) {24res.status(401).json({ error: "Unauthorized" });25return;26}27try {28const accessToken = await getSpotifyAccessToken();29const response = await fetch(30`https://api.spotify.com/v1/me/player`,31{32headers: {33Authorization: `Bearer ${accessToken}`,34},35}36);37if (response.ok) {38const current = await response.json();39const dominantColor = await fetch(`https://bzhou.ca/api/getColor/${current.item.album.images[0].url.split("/")[4]}`).then(res => res.json());40console.log(current);41res.status(200).json({42title: current.item.name,43artist: current.item.artists[0].name,44album: current.item.album.name,45color: dominantColor.answer,46duration: String(Math.round(current.item.duration_ms / 1000)),47progress: String(Math.round(current.progress_ms / 1000)),48paused: String(!current.is_playing),49volume: String(current.device.volume_percent),50shuffle: current.shuffle_state,51loop: current.repeat_state52});53} else {54const errorMessage = await response.text();55res.status(response.status).json({ error: errorMessage });56}57} catch (error) {58res.status(500).json({ error: "Internal Server Error" });59console.log(error);60}61}
This function is a bit more complex but its simply meant to extract all the values I need and return them appropriately.
This route is meant to manage the state of the music player, it would do this by sending a POST/PUT request to Spotify's servers with the appropriate action and subaction.
1import { NextApiRequest, NextApiResponse } from "next";2import getSpotifyAccessToken from "@/utils/functions/getSpotify";3type ESPInfo = {4answer: string;5};6type Error = {7error: string;8};9const changes = ["playPause", "skip", "back", "vinc", "vdec", "loop", "shuffle"];1011async function getPlayerData(res: NextApiResponse, accessToken: string) {12const response = await fetch(13`https://api.spotify.com/v1/me/player`,14{15headers: {16Authorization: `Bearer ${accessToken}`,17},18}19);20if (response.ok) {21const responseData = await response.json();22return responseData;23}24else {25res.status(response.status).json({26error: "Error fetching player status",27});28return;29}30}3132export default async function handler(33req: NextApiRequest,34res: NextApiResponse<ESPInfo | Error>35) {36const { password, change } = req.query;37if (password !== process.env.PASSWORD) {38res.status(401).json({ error: "Unauthorized" });39return;40}41if (!changes.includes(change as string)) {42res.status(400).json({ error: "Invalid change" });43return;44}45try {46const accessToken = await getSpotifyAccessToken();47let url = "";48let metho = "";49if (change === "playPause") {50const responseData = await getPlayerData(res, accessToken);51if (!responseData.is_playing) {52url = "https://api.spotify.com/v1/me/player/play";53metho = "PUT";54} else {55url = "https://api.spotify.com/v1/me/player/pause";56metho = "PUT";57}58} else if (change === "skip") {59url = `https://api.spotify.com/v1/me/player/next`;60metho = "POST";61} else if (change === "back") {62url = `https://api.spotify.com/v1/me/player/previous`;63metho = "POST";64}65else if (change === "vinc" || change === "vdec") {66const responseData = await getPlayerData(res, accessToken);67let volume = responseData.device.volume_percent;68if (change === "vinc") {69volume += 10;70volume = Math.min(volume, 100);71} else {72volume -= 10;73volume = Math.max(volume, 0);74}75url = `https://api.spotify.com/v1/me/player/volume?volume_percent=${volume}`;76metho = "PUT";77} else if (change === "loop") {78const responseData = await getPlayerData(res, accessToken);79let state = responseData.repeat_state;80if (state === "track") {81state = "context";82} else if (state === "context") {83state = "off";84} else {85state = "track";86}87url = `https://api.spotify.com/v1/me/player/repeat?state=${state}`;88metho = "PUT"89} else if (change === "shuffle") {90const responseData = await getPlayerData(res, accessToken);91let shuffle = responseData.shuffle_state;92url = `https://api.spotify.com/v1/me/player/shuffle?state=${!shuffle}`;93metho = "PUT";94}9596await fetch(url, {97method: metho,98headers: {99Authorization: `Bearer ${accessToken}`,100},101});102103res.status(200).json({ answer: "Success" });104} catch (error) {105res.status(500).json({ error: "Internal Server Error" });106console.log(error);107}108}
This part was a bit more complex than it might've seemed initially primarily since I had to account for the different states of the music player and the different actions that could be taken. Simply put, you cannot just tell spotify to loop or unloop or move onto the next state. You have to explicitly tell it to loop the song, loop the playlist, or not loop at all. Similar logic follows for the shuffle state. Therefore I just opted to have it sort of rotate through similar to how the app does it.
This utility route is meant to get the access token for Spotify, it would do this by sending a POST request to Spotify's servers with the appropriate credentials and then parsing the JSON data to get the access token. I need this since access tokens actually expire after a certain amount of time and so this just helps me get the correct credentials forever.
1let accessToken = "";2let tokenExpiration = 0;34export default async function getSpotifyAccessToken() {5const { SPOTIFY_CLIENTID, SPOTIFY_SECRET, SPOTIFY_REFRESHTOKEN } =6process.env;78if (Date.now() < tokenExpiration) {9return accessToken;10}1112const authString = Buffer.from(13`${SPOTIFY_CLIENTID}:${SPOTIFY_SECRET}`14).toString("base64");1516const tokenResponse = await fetch(17"https://accounts.spotify.com/api/token",18{19method: "POST",20headers: {21Authorization: `Basic ${authString}`,22"Content-Type": "application/x-www-form-urlencoded",23},24body: new URLSearchParams({25grant_type: "refresh_token",26refresh_token: SPOTIFY_REFRESHTOKEN || "",27}),28}29);3031const tokenData = await tokenResponse.json();3233accessToken = tokenData.access_token;34tokenExpiration = Date.now() + tokenData.expires_in * 1000;3536return accessToken;37};
In conclusion, I had a really fun time creating this project especially since I was able to combine a lot of my knowledge in programming as well as hardware to create a semi-useful project... I decided to specifically write about this project since it was one of the major reasons I decided to pursue a degree in computer engineering and I thought it would be a good way to show off some of my skills.