Last month, a friend of mine (Bolaji) approached me about redesigning his portfolio website. As a software engineer, music producer, and DJ, he wanted a site that reflected all aspects of his professional life. Having recently started learning design, I was eager to take on this project.
One feature we thought would be particularly interesting was displaying what he was currently listening to on Spotify. This would add a dynamic, personal touch to his site, reflecting his passion for music. However, implementing this feature led us down an unexpected path of optimization and package development.
Adding a now playing feature
While I was explaining the flow to him, he felt that the whole process was unnecessarily complicated and that we could optimize the flow. The existing flow required that I installed Airtable and create a table with some specified column headings and a refresh token that I could authenticate API requests with.
Unknown to me, Airtable had recently migrated from Refresh tokens to Personal Access Tokens (PATs), this highlighted the risks of relying on third-party services for critical functionality. I had to remove Airtable and find a more reliable way to store the tokens.
We decided to find out if there were packages that address this problem but most of what we found were personal implementations that still relied on rolling out your personal storage solution. The official Spotify SDK was quite large (1.6MB unpacked), and even a popular wrapper came in at 141KB. Given that we only needed a few specific methods, these sizes weren't justifiable for our use case.
Bolaji has a product called, Annie, that allows people paste the link of a song from one platform and get a super link that links to the same song on other platforms. Due to this, the idea to create a package that was platform agnostic wasn’t farfetched.
Revisiting the flow
The first thing we did was to break down the existing flow for Spotify into sections for a clearer view.
- You create a new app from the Spotify Dashboard and you get a Client ID and Client Secret.
- You authorize the ID with the scopes that you want. In this case
user-read-recently-played
anduser-read-currently-playing
. - After authorization, you get a code and a refresh token.
- You use the refresh token to generate an access token, that expires in an hour to fetch your data.
- Then, you make a request to the currently playing endpoint
- if you are currently listening to something, it returns the song’s details.
- if you aren’t, and in case of a 204 - No Content and go to step 6.
- token has expired and you have to generate a new access token. To prevent this, you can add a check to see if the token is about to expire.
- You make a request to the recently-played endpoint and you get a result.
Optimizing API calls
Steps 1-3 are done manually so we don’t account for that. Here are the possible scenarios:
- Token is valid, currently playing returns song details: 2 calls
- Token is valid, currently playing returns no content (or not listening), and fetches recently played: 3 calls
- Token is expired, fetches and stores new token, currently playing returns song details: 4 calls
- Token is expired, fetches and stores new token, currently playing returns no content (or not listening), and fetches recently played: 5 calls
This is all minus the API call to kick start this process or potential calls to store and retrieve tokens from a database.
Our optimization strategy involved implementing in-memory storage with an optional caching layer for the refresh token and last retrieved song details. With caching enabled, we reduced the number of calls to 0 for a cache hit and a maximum of 3 for a cache miss when the user isn't currently listening to music.
Now Playing
The result of all these led to building a package called now-playing. We implemented:
- In-memory token storage with optional caching to reduce API calls.
- Cross-platform design for future extensibility
- Performance optimization, reducing average response times by 60%.
Usage
The package is quite simple to use.
import { NowPlaying, Providers } from 'now-playing';
const np = new NowPlaying(Providers.SPOTIFY, {
streamerArgs: {
clientId: process.env.SPOTIFY_CLIENT_ID,
clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
refreshToken: process.env.SPOTIFY_REFRESH_TOKEN,
},
})
console.log(np.fetchCurrentlyPlayingOrLastPlayed()
// Result
// {
// "is_playing": false,
// "title": "Black Genesis",
// "artiste": "Drumetic Boyz",
// "image_url": "https://i.scdn.co/image/ab67616d0000b2738bcd48ba8ce6e6921141d269",
// "preview_url": "https://p.scdn.co/mp3-preview/52b9d1bd464f5c06d444bc3e0f5e3aeec8d80de1?cid=0f980936be88427480fd033354fc67a6",
// "url": "https://open.spotify.com/track/1MkPtc1wavRPiAYqXKbcY7"
// }
The Providers
enum contains the supported providers. Currently, it only has SPOTIFY
as a value, but we plan to add more providers in the future. We initially exposed a StorageKind
enum that allowed you to chose between supported storage options but we decided that it was better to expose an IStorer interface that allows people use their own storage mechanism.
There is a detailed section in the readme on how to get the variables you need to get started with Spotify.
You can start using now-playing today and we would appreciate any feedback you have for us. Your input will be crucial as we continue to improve and expand the package's capabilities.
You can checkout Bolaji's new portfolio to see the implementation.