Meet Marvin

The Problem:

We used to have an office music problem. A silent office can be soulless, and music really helps. We have a TV with a Mac Mini that sits in the office, and we would set up some playlist to play on it at the start of the day and just let it play. In theory it could be changed by anyone, but going over to it and making choices required effort and resistance to close scrutiny. A poor choice could see the office silently seething for hours!

I had some downtime, and had wanted to play with NodeJS and web sockets, so I seized this opportunity.

Taking the Stage

I imagined a solution that allowed everyone to make small changes to a common playlist without leaving their desk and allowed everyone to collaborate throughout the day.

It seemed to me that there were two discrete components: there would be a “player” to be put up on the TV. There should also be a “manager”, that would be a web page that anyone could visit to add items to the queue.

Making the player seemed fairly straightforward. I made a page that was just a full-screen YouTube player. It would communicate via sockets with the server which would fire over YouTube video IDs for it to dumbly load. Throughout development, it didn’t get much more complex than that.

The server would need to handle a queue of video IDs. It would need to send the next ID to the player when appropriate and should accept IDs from a manager to add to the queue.

Because names are hard and unimportant, I codenamed this project Marvin.

NodeJS, Express and Socket.IO

I very quickly created a server with NodeJS, using Express to scaffold a couple of endpoints for a player and manager. I also used Socket.IO to handle communication between all parts. I was quickly able to establish a socket that allowed the server to send a “play_track” message to the player, instructing it to play its first video.

The Marvin player listens for play_track messages

I whipped up a prototype manager which could submit a video ID to the server which would be queued. The player would request a new song when the previous song finished, and Marvin was born!

Now Accepting Requests

Surprisingly, most users don’t have the Youtube video IDs of their favourite songs memorised. I know right! It seemed an obvious step was implementing a simple search form which would (via the server) query the Youtube Data API and display the results, allowing the user to add those results to the queue!

Marvin can search the Youtube Data API

I added a couple of other utilities, like a skip button in the manager which would automatically load up the next song and volume controls to control the volume of the player (mostly to be used by those who had to answer the phone). I also added up and down votes which had no purpose but to allow people to share their excitement about upcoming tracks.

I’m no designer, so I used the Foundation framework to give it a lick of paint and some structure that wouldn’t make a non-developer’s eyes water (or at least make them water less).

Marvin's prototype design

Marvin was ready to give its first performance!

Opening Night

I launched version 0.1 of Marvin to the first wave of testers on a Tuesday morning after a frantic first day’s development.

A couple of requests came up immediately. The skip button needed to be protected from trollish abuse, users asked for the votes to have some actual effect so that more popular songs would play first, and it would be useful to know WHO was adding which tracks.

For the skip button to be policed (I would change it to requiring 3 skip “requests” before a song was actually skipped) and for votes to be meaningfully tracked (and limited to one per person), a login would be required.

Authentication

I was reluctant to have a fully fledged database, so I instead opted to use Facebook login to authenticate users. In fact, I used Passport’s Facebook strategy.

Marvin uses Passport and its Facebook strategy to handle authentication

This login was only used so I had a unique user identifier to track each user’s activity, but it worked fine for that. No user data was persisted at all.

Once this was implemented, it meant that all votes and skip requests could be tracked, and duplicates could be prevented. Requiring three skips next was simple, and the songs could be ordered by number of votes, so that the most popular songs would go straight to the front of the queue.

Because Facebook also provided the user’s name, adding that name to each track was straightforward, and meant that everyone would know who was adding which songs.

Persistence

With all these changes that were being deployed, the server was being restarted a few times, and this highlighted another problem: the queue was simply stored in the server’s memory and not persisted at all.

I integrated Redis support to persist the queue so that when the server was restarted the queue would be there ready when it next started up, complete with votes intact.

When a song is added, Marvin adds its data to the queue in Redis

Giving Marvin a Makeover

The office had responded really well to Marvin and it had become a permanent fixture. Our lead designer Darragh had even spent some time putting together a polished design for it, and I had a couple of volunteer front-end developers, Katie and Keith, to help me implement it.

Just like the project had grown out of an excuse to learn some Node and sockets, I wanted to learn some new technologies from the front-end build. I decided to build the whole front-end using React components.

Breaking the app into separate components allowed some nice clean implementation. For instance, the search form component only cared about presenting a form and sending a “search” socket message to the server. The search results component, on the other hand, only cared about waiting for a “search_results” message, and presenting the data it received.

The SearchResults component listens for search_results messages and updates its state accordingly

Likewise, the queue had multiple items that would obviously be individual components, since each would have to handle multiple actions for the appropriate track.

Once I’d made the components, I handed them over to Katie and Keith who implemented the design around them.

Infinite Stations

The lofty goal I’d set myself was to allow any number of “stations” to exist. There had always been just one which everyone used, but I was able to implement storing multiple queues in Redis and handling players and managers for an arbitrary number of stations. This would allow multiple users to create stations, invite their friends and listen to shared playlists totally independent of each other.

The queues would be stored in Redis keyed by the station’s name, and when the clients (players and managers) registered they would identify which room they were connecting to to allow them to receive the right messages.

If Marvin were to go public, this would be an absolutely necessary piece of functionality.

Clients register with the server, specifying the room/station they want to connect to

The Difficult Second Album

Finally the front-end build was complete, with the swanky new design and fully React-ified structure. With multiple stations supported and a number of other smaller issues fixed and new features implemented, we were able to release a full 1.0 version for internal use.

The change requests keep coming, of course, but the project has moved from a day’s hacking and learning to a fully-featured product that sees constant use.

I hope we can release Marvin to the public soonMarvin

1minus1 wrote this post while regretting his co-workers' taste in music.