jukeboxli, a Spotify based jukebox
by Silas Stulz
jukebox.li is a music jukebox based on Spotify. With Jukebox.li, all 30 million songs on Spotify are available to you and your friends. Create a new room or join an existing one. Together you decide what's going to play next.
Overview For a long time, I had the idea to develop a web-based jukebox-like application. Recently I played around with the Spotify API (check my latest posts). I recognized, that the Spotify API offered all functionalities needed to implement this project. So I just started coding and a few weeks later I now have the prototype ready, which is also the topic of this blog post. I'm going to talk about what you can do with jukebox.li what I'm still working on and which technologies I used and why.
jukebox.li?? Alright, what exactly is jukebox.li? To get the most out of it, you need a Spotify Premium account. With a standard account, you will still be able to use the service, but you won't be able to listen to the songs.
jukebox.li is based on "listening rooms" called "playlists". You can either create your own playlist or join an existing one. You just need the identifier provided by the creator.
In a playlist, you and your friends (or random strangers) can add songs from over 30 million choices to a queue and listen to it synchronously on all your devices.
You can vote on the songs you like so that they get pushed to the top of the queue.
jukebox.li is thought of as a fun little application where you can listen to the same music as your friends and also helps you to discover fresh tracks.
But it's not a web-based Spotify client. To listen to the music you still need Spotify open on a device of your choice.
Architecture
Now that you know what jukebox.li is, what's behind it? jukebox.li is based on three components. An Angular frontend, a Django backend, and the Spotify API. In the following paragraphs, I will talk about these components in depth.
Frontend
The frontend is an angular single-page-application running on a ubuntu web server on a Digitalocean droplet.
I choose Angular because I have some prior experience with it and prioritize it over other JS-Frameworks like Vue.js or React.
The Angular application is responsible to communicate and fetch necessary information from the backend as well as the Spotify API.
It handles all calls to the Spotify API with an exception of acquiring the valid Bearer token. This has to be done from the backend as we don't want to expose our Spotify API Secret. We receive the code which we send to our backend, the backend exchanges this code for a valid Bearer token and returns the token to the client.
Also, a lot of the logical part is embedded in the frontend. For example the sequence of the songs or playing the songs on the device.
The design is based on different bootstrap components put together. I know it looks horrible ;)
Backend
The main reason why I developed jukebox.li, was that I wanted to play around with the Django REST Framework. This is why I choose Django over other frameworks like node.js. As it turned out, Django was not the optimal choice for this kind of project, which I will explain.
But first, let's talk a little bit about the structure of the backend. It acts as a RESTful API.
One of the main tasks is to expose the database and allow CRUD-operations on the models. Most importantly it handles the state the current playlist is in (playing a song, current time).
Another important function is to provide authentication for the Spotify API. As said before we can't get the Bearer token for the Spotify API from our frontend, as it would expose our Project Secret. Thus, we need to do it from the backend and return the token back to the Angular application.
@api_view(['POST'])
def trade_code_for_token(request):
if request.method == 'POST':
try:
code = request.data.get('code')
except AttributeError:
return Response(status=status.HTTP_400_BAD_REQUEST)
auth_str = '{}:{}'.format(CLIENT_ID, CLIENT_SECRET)
b64_auth_str = base64.b64encode(auth_str.encode()).decode()
header = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': 'Basic {}'.format(b64_auth_str)
}
body = {'grant_type': 'authorization_code', 'code': code, 'redirect_uri': REDIRECT_URI}
r = requests.post(AUTH_URL, data=body, headers=header)
answer = r.json()
refresh_token = answer['refresh_token']
access_token = answer['access_token']
data = {'access_token': access_token, 'refresh_token': refresh_token}
return Response(data)
Websocket
Now the Django REST Framework is awesome for classical APIs. But an application like a jukebox should be very responsive, for example, if a user adds a new song, all the other clients need to update their queue as well.
So, in addition to the HTTP connection, the Django backend also needed to act as a WebSocket server.
A WebSocket connection is a bidirectional connection between a server and a client. This enables the server to also send updates to the client without a request.
I choose Django Channels for this task to integrate a WebSocket server. Now in this scenario, the client not only has to talk to the server but also to other clients. To group the clients, which are in the same "room" or "playlist" I used a channel layer. Now if any message is sent to the server it's automatically broadcasted to any client in this room. To make this work, I also needed a Redis instance as its backing storage.
All in all the solution works. But if I would do this again, I would probably go with node.js and socket.io.
Configuring NGINX
The thing I struggled the most was configuring my NGINX server to redirect Websocket requests to the specific port. Because there is no good documentation out there on how to do this. I was on this for a few hours and was desperate. In the end, it was a pretty easy and straightforward solution, which made it even worse. But hey, sometimes you get stuck on the problem and can't even see the easy solution in front of you. Here's the NGINX conf in case you're interested. The point is that all requests to /ws get directed to the right location.
In the process, I even thought about implementing a separate Websocket server like socket.io but I decided against it because I thought it would complicate more things than help. There would be another component that would need to be integrated into the existing system and I wanted as few components as possible. Also, I really wanted to solve this problem.
Spotify API
I will not talk about the Spotify API in this blog post. You can find a lot of information about the Spotify API in my previous posts or in on the official website Spotify API
Summary
I know this was rather a quick walkthrough for the technical part. I will probably write about some of the technical aspects (NGINX, Django Channels) more in-depth in feature posts. That's it for the moment. Feel free to try it out and shoot me some feedback or bugs. jukebox.li. Also if you have any Ideas on how I could improve the application, I'm open to any ideas.
The whole project is available on Github, on there is also a list of things and tasks that I want to implement. If you want to contribute, check it out!