Go to Home
JavaScriptNodeAPIs

Playlist Assist - Spotify Auto-DJ

Spotify companion app that creates beat-matched transitions within your Spotify playlist. Remotely controls your Spotify account from a website.


Digital DJ-ing

Many people curate their playlists, but few think about the transitions between songs. While it's easy to appreciate two songs that flow together seamlessly, trying to organize this effect in a playlist is a lot of work. Playlist Assist does it for you. I built this webapp during the final few weeks of my senior year of highschool as part of my school's “Senior Independent Project” program.

Playlist Assist integrates directly with your Spotify account and analyzes the songs you're listening to. It reorganizes your playlist in the queue to improve flow, then automatically skips between songs and points that sound extremely similar.

Week 1: User Interface & Timer System

While this project did not need a robust UI, I wanted it to show more than just what was currently playing. I wanted a way to display the user's queue and post logs for what the app was doing. For the queue I settled on just a stack of cards on the lefthand side of the screen that contained song information. I got this info from the Spotify API that lets you get the metadata for a batch of song IDs. For the logging I thought a terminal-styled window on the right of the screen would look cool, so that's exactly what I did.

UI

To smoothly transition between songs, Playlist Assist needs to accurately measure the user's position within the current song. Without knowing the user's current timestamp, you can't know when to make the jump between songs.

Position

The Spotify API lacks support for webhooks and long-polling so I had to set up a local timer to track this. Usually I would just use javascript's built in setTimeout() function, but since it has a drift I need to build one that accounts for drift on every iteration.

export default function Timer(callback, timeInterval, errorCallback) {
    this.timeInterval = timeInterval;
    this.start = () => {
        this.expected = Date.now() + this.timeInterval;
        this.timeout = setTimeout(this.round, this.timeInterval);
        console.log('Started timer');
    }
    this.stop = () => {
        clearTimeout(this.timeout);
        console.log('Stopped timer');
    }
    this.round = () => {
        let drift = Date.now() - this.expected; // the drift
        if (drift > this.timeInterval) {
            if (errorCallback) errorCallback();
        };
        callback();
        this.expected += this.timeInterval;
        this.timeout = setTimeout(this.round, this.timeInterval - drift);
    }
}

Even though the timer is accurate, I'll still want to periodically synchronize with Spotify. I'll do this to make sure that if the user skips ahead in their song the timestamp change is accounted for.

Week 2: Pre-processing & Beat-Finding

To ensure the smoothness of transitions, songs in the playlist need to be pre-processed, I call this phase 'ingestion'. The objective here is to rearrange the playlist so that similar-sounding tracks are near eachother, creating a 'flow'. It is important to note that the user's current song will always remains in the first position during ingestion.

The rearrangement begins by retrieving tracks' information from Spotify and converting them to vectors.

vectors

Once the songs are represented as vectors, comparison can be done using cosine similarity, euclidean distance, or another norm. I chose euclidean distance because it was easiest to implement and cosine similarity is generally less accurate for lower dimensional vectors, but it may not be the best option here.

Starting with the currently playing song, we iteratively compare it with all the other vectors in the queue, selecting the best match to follow the first song. Then, this process is repeated recursively starting from the newly placed song until the playlist is fully organized.

Week 3: Refactoring

The third week of this project was spent reorganizing/improving my code and adding small features like a pause button. Playlist Assist was the first larger scale project I worked on so I still had a lot to learn about how to organize a project.

Week 4: Beat Matching & Transitioning

To identify timestamps that can be transitioned, I need to get more specific information about a song from the Spotify API. Songs on Spotify are split into a hierarchy (bars, beats, sections, segments, tatums) where bars are the largest intervals and tatums are the smallest. Each group has its own data that can be accessed. For this project, segments had the most relevant data.

I looked to compare two songs' segments by converting each segment to a vector then comparing them similarly to how I handled ingestion. By doing this I could rank all the possible jumps in a song and then pick out the two segments that sounded most similar to use as a jump.

This is an example of what a single segment's JSON looks like: Example segment

Once a jump is found, it's handed off to the timer so Playlist Assist can know when to jump.


Flycatcher - Waitlist Builder
TypeScriptNextJSTailwindCSSMongoDBUploadThingSupabaseStripeMailgun

Flycatcher - Waitlist Builder

Build your custom branded waitlist, collect insights about your subscribers, and send email campaign...

TOS Chat - Custom AI Chatbot
TypeScriptNextJSTailwindCSSPineconeLangChainMongoDB

TOS Chat - Custom AI Chatbot

Custom GPT chatbot that allows the user to 'chat' with terms of service and privacy policy agreement...

All Posts