Risu | Cloud Storage + Cloud functions

Dev time: 9 days
Project: https://github.com/datyayu/risu.moe
Latest commit: 2aec74b

This post comes a week after the playlist / file upload module was “done” because I was being lazy IMPORTANT REASONS.  Although the code may not be as fresh in my head as I would like, I’m going to give you an overview of how the file upload is handled, both client side (with the firebase sdk) and server-side (with cloud functions).


As always, here’s how the current iteration looks:

Screen Shot 2017-05-22 at 7.33.39 PM.png

There’s only one minimal change that you may not notice unless you compared this screenshot to the old ones: The playlist now includes new songs. This may not look very relevant but this because this current version pulls the playlist straight from the firebase real-time database.

This database is filled by files that had been updated via the drag and drop interface.

Screen Shot 2017-05-22 at 7.40.38 PM.png

The flow here is:

  1. User drags file into the window and we get the file info.
  2. File gets uploaded using cloud storage via the firebase’s sdk
  3. Whenever a file is added to the storage bucket, firebase triggers a cloud function that creates an entry with the file’s info on the real-time database.
  4. User gets notified when then database is updated and elm displays the new song.

So let’s checkout some of the main parts in each side.


Drag and drop

Drag and drop is handled using simple event listeners

document.body.addEventListener('drop', function(evt) { ... })

On the drop listener, first we need to stop the browser from opening the file and trying to play it, exiting our page. To do this, we simply call

evt.preventDefault();

This will stop the browser from opening the file, and will also allow us to handle the event inside our listener.

Once we got control of the event, we can access the file using

var files = evt.dataTransfer.files;
var file = file[0]

Since evt.dataTransfer.files gives us an array of files (more than one may be dragged at the same time), we select the first one, as we really only care about one file (multifile drop support may promote really long queues and music spam, also I want to keep it simple).

Before uploading, for our specific use-case, we need to get the track duration.  In order to get this info, we need to load the file. The way I implemented this is by creating an audio element were we can load the file and get the duration.

var audioElement = document.createElement('AUDIO');

var fileUrl = URL.createObjectURL(file);
audioElement.src = fileUrl;
audioElement.addEventListener('canplaythrough', uploadFile, false);

function uploadFile(evt) {
  audioElement.removeEventListener('canplaythrough', uploadFile, false);
  // Get the duration
  var duration = evt.currentTarget.duration;
  // ... Other stuff
}

We create the element, a valid url for our src using URL.createObjectURL, and then we add an event listener for canplaythrough.

The reason we do this is because we don’t get the duration as soon as we set the src in the audio element. We need to wait for the browser to parse the file and update the audio element with the file’s data. When the browser’s done with that, it will emit the canplaythrough event and there’s where we can get the info we need.

You may also notice that I removed the listener as soon as the event gets triggered. This is to avoid having unnecessary listeners and also avoid having duplicated listeners being fired at the same time whenever we load another file.


Uploading files using the firebase SDK

Finally, once we get all the info, we just use the sdk to upload the file.

 var uploadTask = storageRef.child(refName)
                            .put(file, { customMetadata: metadata });
 uploadTask.on('state_changed',
  function update(snapshot) {
    // Notify elm the progress.
  },
  function error(e) {
    // Notify elm it failed.
  },
  function success() {
    // Notify elm it succeeded.
  }
)

We upload the file by creating a ref and then pushing that file to the cloud. Firebase also allows us to pass custom metadata (such as the title, the duration, the user who posted it, etc) using the customMetada attribute inside the metadata we pass to it. This will allows to retrieve that info later on our cloud functions.

The final part of this step is to notify elm as we upload the file. In the example above, the update function takes a snapshot, which we can use to access the current progress of the upload, and display it to the user using elm. Similarly, we modify our UI based on whether the file upload finishes or fails.


Updating the database on file uploads using cloud functions

We want to keep track of every song that gets uploaded. The way we do this is that we take advantage of the firebase cloud functions’ listeners and we listen for changes to our file bucket, and then we add a new entry to the database when a new file is added.

exports.updatePlaylistOnNewSong = functions.storage.object()
  .onChange(function (event) {
    // ...
  })

The way we listen for those events in a cloud functions is by attaching an onChange listener to functions.storage.object(), which represents a reference to the whole storage bucket, so whenever something gets added, this will be called.

First thing we do as soon as this gets called, is to make sure to only take into account whenever the song is added. If the resourceState equals to ‘not_exists’, it means that the file has been moved or deleted, so we exit in that case as we don’t care about it.

// Exit if this is a move or deletion event.
if (event.data.resourceState === 'not_exists') {
  return;
}

Once we are sure the event is the one we want, we can create an entry in the database to represent that file.

const metadata = event.data.metadata;
const entry = admin.database().ref('/playlist').push();

entry.set(metadata);

We create a new entry using push and then we just fill it using the set method.


Reflecting the database changes in elm.

In order to get the playlist from the database, we use the ‘value’ listener. The reason we use ‘value’ and not ‘child_added’ like with the chat it’s because we want to know whenever the playlist is modified in any way, so we can show the user the current playlist state at any time.

var playlist = database.ref('/playlist');

playlist.on('value', function(snapshot) {
  // ...
});

Of course, once we got the playlist value, we can just send it to elm using ports:

app.ports.setPlaylist.send(songs);

And that’s most of what was involved into the playlist module. I think this one may be modified once I get into the player module, because of how interrelated they are, but we’ll see once we get there.

Also, event thought the client side logic it’s written in elm, I just realized that I have barely talk about it. I think this is because elm’s logic and flow seems so natural and simple that I kinda feel like there’s not much to explain about it. Most of the stuff it’s either view functions ( Model -> Html Msg ) or update functions ( Msg -> Model -> (Model, Cmd Msg) ) , so it’s pretty hard to find something to talk about when most of the code it’s so simple to understand if you know the elm architecture. I’ll probably do an overview about the elm code structure once I’m done with the player module, but as a I said, I don’t think there’s a lot to learn from that since it’s pretty standard stuff.
Anyway, that’s it for this post, next task it’s to actually be able to play the songs we upload and keep the playback (kinda-)in-sync across clients. I’m still not sure of how to I’ll accomplish that, so this may take some time, but once I’m done I’ll write in-deep about whatever solution I found for that.
Cya next time.
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s