Intro
For those of you subscribed to me for well thought-out emotional posts, I have some bad news for you. This is a technical article! But I'll do my best to make it readable and fun 😃
As a brief reprieve from my Capstone preparations, I decided to look through Raymond Gan's Build Your Own X repo for a worthy weekend project. Since I didn't have enough time for some of the more intriguing entries, I decided on building a Discord bot. If you know me, you know I've been a hobbyist developer since I was ~15, so naturally I've made a Discord bot before.
What I haven't done, is really made a Discord bot. I've followed tutorials and made ping-pong bots, but nothing beyond that. So I wanted to challenge myself to make a bot that I, and even some friends, would actually regularly use. Enter Playlistify. Logo below for thumbnail purposes.
What I haven't done, is really made a Discord bot. I've followed tutorials and made ping-pong bots, but nothing beyond that. So I wanted to challenge myself to make a bot that I, and even some friends, would actually regularly use. Enter Playlistify. Logo below for thumbnail purposes.
Origin
One of my favorite activities that I've been a part of the past few months has been the music club that sprouted out of Launch School's #the_social_network Slack channel. Every week, myself and a handful of other students hop in a Discord call and talk about music. It's been a great way to get to know my fellow classmates and discover awesome new artists. Each week we pick a theme (like live albums, international artists, etc.) and each one of us shows up to the meeting with an album recommendation.
The Problem
My friend Lucas has been compiling these weekly recommendations on his personal Spotify account and sharing it with us. Initially this was a fairly simple task, but as the group continues to grow it has become more and more cumbersome to manage these playlists himself. In addition to this, we also had the idea for a monthly playlist that we all can add fresh finds to, whether it's new music from that month or a new personal discovery. While all of this is manageable via any Spotify client, it can take us (and by us, I mean Lucas) out of the normal meeting flow when we have to add albums to this playlist. Or, as is the usual case, not adding them during the meeting but a few hours later, when it might be harder to remember all the recommendations.
Another thing to think about is club management. Say Lucas gets busier as he progresses in the curriculum and steps down from leading (using the term loosely, don't tell Lucas 😉) the music club. Now, someone else has to use their Spotify account, and there's not one location for an archive of our playlists.
Another thing to think about is club management. Say Lucas gets busier as he progresses in the curriculum and steps down from leading (using the term loosely, don't tell Lucas 😉) the music club. Now, someone else has to use their Spotify account, and there's not one location for an archive of our playlists.
Research
From our problem, I've extracted a few specific goals that I want to achieve with Playlistify:
- seamlessly add a song/album to a playlist without disrupting meetings
- those who aren't familiar with Spotify client UIs past the basic listening usage should be able to collaborate on playlists
- create a "communal" Spotify account to host our playlists
Goal number 3 is easy and straight-forward. I imagine if you're reading this on the Internet, you also understand how to register for various accounts. Even though it's simple, the account is where all of our playlists will be stored so it is still a very important step in the process. With this in mind, let's look into Spotify's API and understand how an application interacts with a user account.
Authorization
The Spotify API has multiple authorization flows depending on the scope and intent of the application. Some endpoints need only a client token which identifies the application, whereas other more restricted endpoints need a user token granting access to that user's account.
Since the primary business logic of this application won't be running in the browser, I won't be able to use a one-time token provided by the Implicit Grant authorization flow. I also know I'll need to access endpoints that require user approval. This narrows it down to two flows, one of which requires an extra verification step to avoid storing client tokens at the application layer (which can be unsafe for Mobile or Desktop applications). So the best choice is the general Authorization Code flow since my application will be a service running on a private server.
You can see my beautiful log in page below. I've heard simplicity is one of the most important factors for designing a good UX, so I made sure to create a frictionless experience.

This page redirects the user to a login page on Spotify's site, which then redirects the user back to a URL that you've whitelisted on a successful login. The reason I didn't spend time really making a front-end for this portion of the app (other than it not being the focus) is that the Authorization Code flow also gives us a refresh token that we can use to request a refreshed access token. Because of this, I only need to log in to the account once and then occasionally send a request for a refreshed token and update the token I've stored in my database.
Data
I'll spare you the nitty-gritty details of which endpoints I need to access with what data, partially because it's fairly straight-forward. As I mentioned before, we have an access token tied to the user we've logged in with that we'll need to include in our Authorization HTTP header. Beyond that, it's as simple as making the proper HTTP request to the proper endpoint with that token and a song, album, or playlist identifier depending on our intent.
As far as what we're storing in our database, it's also straight-forward. A row should contain both the refresh token and the access token for that user, along with the IDs for the current weekly and monthly playlists, and as expected the user's ID as well. If this were a more robust application with many users, I would actually take the time to come up with a proper schema. Since this is a simple MVP with one user, I defaulted to one table.
Application Architecture
After researching the necessary resources, I drafted up my initial application architecture.

In the image above, I built a basic flow of the business logic. There would be two "active" processes going on at any given time: the websocket connection to Discord (handled by the DiscordJS library) which listens for messages, and the occasional refresh token request to the Spotify API. Additionally, there would be the user interaction portion of the application.
I decided on two commands: one for the weekly playlist and one for the fresh finds playlist. Each command should take one Spotify URL containing a track or album ID (retrieved via the Share dropdown in a Spotify client). The fresh command should additionally be restricted to just songs, since it spans a whole month and I don't want it to get overloaded.
Once a message is sent, the application should determine if it is a command, and then proceed accordingly by parsing the resource ID and making a request to add that resource to the proper playlist.
Build
With this architecture in mind, I set out to design the multiple classes I would be using.
Database
Since this is a light application, I decided on SQLite for the database. The class is built on top of the sqlite-3 NPM package. In the screenshot of the test cases below, you can see that there are two abstract methods, one for running a generic SQL command and one for running an SQL SELECT command. The separate command for SELECT exists because the sqlite-3 package's #get method returns a result row, whereas their #run method does not. These class methods also promisify their methods. We then have some specific commands for updating and retrieving specific columns.

API
The next class on the list to design was an API for interfacing with Spotify's API. The first method simply appends the desired endpoint to the base URI, and the second method encodes a JSON object into a URI component string. Beyond that, we have our methods that interact with the API. The only one that isn't clearly described by the test is #withForm, which is a function factory that returns a function with the proper structure to send a POST or PUT request. This is utilized by the generic #post and #put methods, along with #refresh and another un-testable #authorize method (since it requires Spotify's user login flow).

PlaylistManager
Up next is the nicely packaged together business logic - a class that handles both API calls and Database updates. Most of these methods are fairly straight-forward. There are some helper methods for parsing and extracting data, and the necessary CRUD functions for creating and updating playlists.

Discord
I don't have test cases for the two classes I made for the Discord API because they require an active websocket connection, and I didn't have the time to create a mock client for testing. What I do have is a nice diagram, albeit slightly inaccurate, I used when designing my classes.

In the diagram above, the Client class extends the Discord.Client class provided by the DiscordJS library. I ended up needing to add a few more methods to the Client, including argument validation and channel binding. I also moved the command parsing method to the Client as well, that way the Messenger class only handles the sending of the messages.
Conclusion
First I'd like to say thanks for reading! This was a fun little side project that is actually going to be pretty helpful. If I ever have the time, there's definitely things I'd like to change/add to Playlistify to make it more generic and useable for more people, but it fits my specific use case so I'm happy with it.
Between the PEDAC process I've learned to utilize at Launch School and Test-Driven Development, this was one of the most seamless and non-headache inducing apps I've ever written. It was actually a little astonishing at how easy it was to go from class design, to testing setup, to writing the classes, to testing everything and it mostly passing on a first run-through. There's nothing more satisfying than all those little green check-marks.
You can find the source code on my GitHub.
Here's a couple screenshots of it in action. Once again, thanks for reading and have a great week!