Friday, October 19, 2012

Pythonisms and WebMote - Part 3

I've had a, shall we say, pretty busy two weeks, but I still managed to do some half-way decent work on Web Mote. The process is teaching me small things as I go, both about the language I'm currently using[1], and about the architectural approach, so I wanted to organize them in my own mind.

Pythonisms

First off, just to make sure I'm clear, Python is definitely in the "Popular" side of the Powerful/Popular continuum I described a while ago. If you're dealing with something that isn't a fundamentally unsolved problem, chances are there's a library that resolves it fairly well. Because the church of OO still seems to be going strong, you'll probably wind up needing to subclass one or two somethings to get the behavior you want, but there's still a lot to be said for just having Good Enough™© solutions lying around at your disposal.

A couple more odd syntactic corners are showing up as I do more involved Python code. Nothing huge, but I've had to look them each up at least once, so lets get this over with

Nested Loops

Are broken out of with return. This shouldn't really come up very often[2], but in case you ever need to, this is how you do it.

def pollingLoop(foo):
    for a in foo:
        while True:
            if bar():
                baz()
            else:
                return "Done"

That return will return from pollingLoop, incidentally terminating both the for and the while. Again, it seems rare enough that I'm not about to complain for lack of more fine-grained flow control. The only place I could think of using this idiom off-hand is in a polling situation, which is how I was originally using it.

Functional Shuffling

The standard random.shuffle function is destructive, which typically isn't what you want when you're trying to be functional. Luckily, you can use sorted to fake a Good Enough™© functional shuffle by passing it a random key.

import random

def shuffled(aList):
    return sorted(aList, key=lambda e: random.random())

Checking Membership

The idea of member? is a primitive to me, but there's no such function in Python. You instead need to use the standalone in syntax.

>>> 1 in [1, 2, 3, 4, 5]
True
>>> "Bob" in ["Alice", "Bradley", "Charles"]
False
>>> 

It's an infix boolean for some inscrutable reason, but it does the job, and is presumably really fast since it's a keyword rather than a function.

Separating Client and Server

About half the point[3] of Web Mote is doing some light experimentation on a particular architectural approach. I've made changes to the front-end which lets me play an entire directory[4], and I'm beginning to ask myself what the correct way of separating that behavior is. There are options, and I'll start by outlining the way it's currently implemented[5]

Semi-Client

Client-side sends a target, which can be either a file or a folder, and the server handles playing it. A file is merely passed to the player, a folder gets searched recursively for all files it contains, and those files are then sent in sorted order to the player to be played one at a time, but note that this decision is made by the server.

It's semi-client because the client doesn't particularly care what message it's sending or what responding to it entails. For instance, if we're shuffling, it would be convenient to display the current file, and a list of previous/next files. In the Semi-Client architecture, the server would start up a player, then report this play queue order back to the client for display. This keeps clients somewhat interchangeable, since the current play queue can be fetched by anyone connecting in.

A problem this might raise later is that if we decide to change the behavior of the shuffle function, or add a playlist, we'll need to make extensive changes on both the server and client sides[6]. Further, the server and client need to synchronize in various non-trivial ways which complicates their communication even if we change nothing else.

Client-Oriented

This solution would involve doing all relevant work on the client. We wouldn't send a target to the server, we'd send a filename. The way we'd handle playing or shuffling a directory would be by asking the server for its deep file list, potentially shuffling it on the client side, then sending the list off to the server for playing one file at a time.

Going down this path radically simplifies the server. It has to expect exactly three kinds of requests:

  1. Gimme the file list of directory (optionally, make it a deep list)
  2. Play file right now
  3. Send command to the running player

Ok, we do also need to be able to ping the client in some way to notify them that file is done playing, but that seems like it could be trivially done by long-polling the play request. If you want to get slightly fancier, for instance in the case where you want to be able to deal with multiple simultaneous clients, you can use SSEs or Ajax polling to send out a done signal when you need to. You may also need to support session/option-caching, but if you want to do it right, you'll probably be using cookies rather than any server-side storage options. Ideally, the server doesn't have to care about anything the client does or keeps track of.

The downside is that the client is suddenly expected to be very smart. If doing Ajax-based JSON communication didn't already commit you to mandatory JavaScript[7], this technique would be the point of no return. Because if your client needs to be the smart component, it needs to be somewhat stateful, and it needs to manipulate its DOM in various ways. You still could write a minimal client that attaches to a server like this, but emphasis would be heavily on "minimal", both in terms of interactivity and in terms of available control options. The only other concern with this approach is that clients are suddenly not interchangeable; if I use my phone to queue up a playlist, then sit down at my laptop, I either need to keep my phone on, or I need to duplicate that playlist on my laptop in order to keep the media going.

Server-Oriented

This solution involves a disproportionate amount of server-based work, and it's about as close as you can get to a traditional web site while still using the separated architecture. Your client can suddenly be almost as dumb as a post, only needing to be able to accept UI changes through JSON feeds. You store any kind of stateful information on the server, which means that you've got a central place to save playlists and such. A shuffle would be implemented more or less as in the Semi-Client solution, but it wouldn't bother streaming back state updates. Rather, the client would make periodic requests of the style "What are you doing now?" and display the results. The same thing would be true for playlists and similar behavior; we would store them in a server-side database somewhere and send status updates out to clients as requested.

It maintains a higher level of decoupling than the Semi-Client solution, and simplifies the client enough that a slightly clunkier, pure-HTML version starts looking feasible.

The main downside is that the server needs to send much more exhaustive readouts of its state. Which means more, and potentially more complex, handlers.

Decision Time

I can't take the Server-oriented option seriously, because it would nudge the shape of the application much closer to a traditional web app. It might also introduce one or two dependencies across client and server as well as greatly complicating the server, and almost-significantly simplifying the client. This does not sound like a good trade.

The current codebase takes the Semi-Client approach, but I'm not too keen on keeping it because of the extra coupling it demands between client and server operations. Playing a list of files properly also needed a surprisingly large amount of work[8].

The second option, the Client-oriented one seems like the correct approach. It complicates the client, but not excessively. It greatly simplifies the server, saving me from needing to deal with even basic multiprocessing concerns for the purposes of actually playing media. Finally, it keeps the client and server completely decoupled, making it even simpler to release new clients and keeping client data entirely out of the servers' reach.

Ok.

I'd better get on that.


Footnotes

1 - [back] - That'd be Python at the moment. And some people at the Hacklab open houses asked, so I guess I'd better clarify again, I don't have any particular affection for Python. It's just everywhere and not horrible, so I figured I may as well. I'm still honestly attempting to line up a Haskell and CL port as soon as I can possibly manage it.

2 - [back] - And in fact, the place where I used the idiom has been re-written such that the inner loop is in a secondary function.

3 - [back] - The other half is split between making use of old, closed hardware I have lying around and fulfilling the next part of my personal crusade aimed at putting a web server in everything within my reach.

4 - [back] - And at least theoretically shuffle it, but there isn't a front-end for that yet.

5 - [back] - It's not necessarily staying this way, but if you do a checkout from that github as of this writing, this is what you're getting.

6 - [back] - Which is precisely what we wanted to avoid.

7 - [back] - Or a desktop/mobile binary client, to be fair, we did briefly mention that option in part one.

8 - [back] - Granted, a lot of this complexity was a result of needing to use different players in different situations, but still.

Monday, October 8, 2012

WebMote the Right Way™© Part 2, or "Controlling my Media via WiFi and RasPi"

And we're back. Part one was the previous article, so I'm not going to bother linking you to it. This time around we're taking a look at one possible back-end to the web-based media interface we cooked up last time. The github has been updated, by the by. First off, there ended up being some change required to the front-end, mostly for browser compatibility purposes.

First, some browsers apparently don't pick up content from a jQuery ajax request. I ran into this with an older version of Safari, and comparably recent versions of Conkeror. If you try to do

$.ajax({url: "foo", 
        data: {...}, 
        success: function (data) { 
           alert(JSON.stringify(data)); 
        }});

what you'll get is not "Stuff the page sent you", but rather "". I have no idea why this is; checking the return value from the server confirmed that it was sending the correct stuff, but it wasn't making it to client side for some reason. I know it's a browser issue because Iceweasel and Chromium both did what I was expecting them to and returned the request contents. I solved that by using the jQuery jqXHR object return value available in reasonably recent versions of the framework. In other words

$.ajax({url: "foo", 
        data: {...}, 
        success: function (data, status, jq) { 
           alert(JSON.stringify(jq.responseText)); 
        }});

No idea why this works while the earlier doesn't, but there you have it. The other problem I ran into was that older versions of mobile Safari don't respect position: fixed;. That doesn't sound like it'll be a big deal to most people, but I specifically wrote Web-Mote so that I could use my first-gen iPod Touch as a remote.

Styling the control panel at runtime solves that problem, assuming the stylesheet doesn't have any position settings.

// older versions of safari don't like `position: fixed`.
// they also don't like when you set `position: fixed` in a stylesheet,
//   then override that with inline styles.
// what I'm saying is that older versions of safari are assholes
if ($.browser.safari) {
    $("#controls").css({ "position": 'absolute' });
    window.onscroll = function() {
        $("#controls").css({ 
            "top" : window.pageYOffset + 'px'
        });
    };
} else {
    $("#controls").css({ "position": 'fixed' });    
}

Finally, it turns out that there's at least one case where we'll be rendering a directory, but not want the play/shuffle buttons. To that end, the protocol needs to change very slightly to accommodate a field specifying whether to render a button. The template needs to change too.

    <script id="tmp-folder" type="text/x-handlebars-template">
      <li class="{{type}}">
        {{#if buttons}}
        <button class="play icon play" onclick="mote.play('{{path}}')"></button>
        <button class="shuffle icon shuffle" onclick="mote.shuffleDir('{{path}}')"></button>
        {{/if}}
        <a class="dir-link" href="#navigate{{path}}">{{name}}</a>
      </li>
    </script>

Oh, actually, I also ended up making those handler changes mentioned last time. WebMote now has exactly four required handlers[1]:

  • /show-directory (a zero-parameter request gets the root directory)
  • /play
  • /shuffle-directory
  • /command

That's it for changes to the front-end since last time, but let me share some random thoughts before going on to the server-side.

Interlude - The State of Lisp Web Development on ARM

My usual web development stack is Hunchentoot on top of SBCL, which turns out to be a problem. You may have noticed that there's no ARM port in that SBCL link. Not "no Debian package", no port period. I guess I could technically grab the source, and laboriously gcc up my own, but I'm honestly neither patient nor smart enough to. GCL doesn't play nice with quicklisp which kind of makes that a non-starter for me regardless of how mind-bogglingly fast it claims to be, CMUCL requires a working CMUCL system to be built from source and isn't in the Wheezy repos, which leaves CLISP[2].

Which I would use if it played nicely with external-program. Sadly,

*** - CLISP does not support supplying streams for input or output.
The following restarts are available:
ABORT          :R1      Abort main loop

Meaning that I could spawn an instance of mplayer or omxplayer, but I wouldn't be able to communicate with it after the fact.

Woo.

Anyway, the long and the short of it is that putting together a Common Lisp solution to this problem on an ARM machine is pretty far from trivial, involving one of

  • manual installation of Hunchentoot[3]
  • resolving the CMUCL cyclical requirements graph
  • compiling your own SBCL

Which is why this first stab is written in Python, and a follow-up is probably going to be using Haskell rather than CL.

WebMote the Right Way™© -- Server Side

First off, have an updated tree

web-mote
├── conf.py
├── LICENSE
├── README.md
├── static
│   ├── css
│   │   ├── custom-theme ## same as last time
│   │   ├── icons ## this too
│   │   ├── style.css
│   │   └── watermark ## and this
│   ├── js
│   │   ├── backbone-min.js
│   │   ├── handlebars-1.0.rc.1.js
│   │   ├── jquery.min.js
│   │   ├── jquery-ui-1.8.13.custom.min.js
│   │   ├── underscore-min.js
│   │   └── web-mote.js
│   ├── show-directory
│   └── web-mote.html
├── util.py
└── web-mote.py

You can easily find this over at the github repo, of course, but I wanted to let you know what you were in for. There's only three files to go through, and we'll tackle the meat first this time around.

## web-mote.py
from subprocess import Popen, PIPE
import web, os, json
import util, conf

urls = (
    '/show-directory', 'showDirectory',
    '/shuffle-directory', 'shuffleDirectory',
    '/play', 'play',
    '/command', 'command',
    '.*', 'index'
)
app = web.application(urls, globals())

class showDirectory:
    def POST(self):
        if web.input() == {}:
            res = util.entriesToJSON(conf.root)
        elif web.input()['dir'] == "root":
            res = util.entriesToJSON(conf.root)
        else:
            res = util.dirToJSON(web.input()['dir'])
        return res

class shuffleDirectory:
    def POST(self):
        web.debug(["SHUFFLING", web.input()])

class play:
    def POST(self):
        try:
            playFile(web.input()['file'])
        except:
            web.debug(web.input())

def playFile(aFile):
    if os.path.exists(aFile):
        if conf.currentPlayer:
            conf.currentPlayer[1].terminate()
        t = util.typeOfFile(aFile)
    ## mplayer suicides if its stdout and stderr are ignored for a while,
    ## so we're only grabbing stdin here
        conf.currentPlayer = (conf.player[t][0], Popen(conf.player[t] + [aFile], stdin=PIPE))

class command:
    def POST(self):
        cmd = web.input()['command']
        if conf.currentPlayer:
            (playerName, proc) = conf.currentPlayer
            proc.stdin.write(conf.commands[playerName][cmd])
            if cmd == 'stop':
                conf.currentPlayer = False

class index:
    def GET(self):
        raise web.seeother("/static/web-mote.html")

if __name__ == "__main__":
    app.run()

It's a very simple web.py application that does the job of spawning external OS processes, then feeding them input based on user clicks on the front-end. I'll assume the routing table, app.run() call and import statements are self-explanatory. web.py routes requests to various named classes which are expected to have POST and/or GET methods attached. If you know the basics of how HTTP works, it should be obvious why.

The index handler at the bottom there just routes a request to our "static" front-end. showDirectory expects a pathname and returns a list of contents of the target[4] using a bunch of toJSON utility functions from util. shuffleDirectory is currently a no-op that echoes something to the debug stream. play attempts to playFile its argument, and prints a debug statement if that fails. The only two interesting constructs here are playFile itself and command.

def playFile(aFile):
    if os.path.exists(aFile):
        if conf.currentPlayer:
            conf.currentPlayer[1].terminate()
        t = util.typeOfFile(aFile)
        conf.currentPlayer = (conf.player[t][0], Popen(conf.player[t] + [aFile], stdin=PIPE))

playFile first checks whether the file its being asked to play exists[5]. If it does, then we check whether a player is already active, and kill it if it is[6]. At that point, we check the type of file we've been passed and start a player based on that. This'll be explained once we go over conf.py, but just to save you the suspense, it's because we want videos running in omxplayer while audio files play in mplayer.

class command:
    def POST(self):
        cmd = web.input()['command']
        if conf.currentPlayer:
            (playerName, proc) = conf.currentPlayer
            proc.stdin.write(conf.commands[playerName][cmd])
            if cmd == 'stop':
                conf.currentPlayer = False

command expects a POST argument called command, uses it to look up a value in conf.commands according to which player is currently active, then writes the result to the active players' input stream[7]. You'll note that I'm representing a player as a (name, process) tuple; I could have made a singleton object, or a dictionary, but this is the simplest representation that works at the moment. Now that you understand the logic, we won't learn anything without taking a look at that configuration state.

## conf.py
from subprocess import call

ext = {
    'audio': ['mp3', 'ogg', 'wav'],
    'video': ['mp4', 'ogv', 'mov', 'wmf']
    }

root = ["/home/inaimathi/videos",
        "/home/inaimathi/music"]

commands = {
    'mplayer':
        {'rewind-big': "\x1B[B", 'rewind': "\x1B[D", 'ff': "\x1B[C", 'ff-big': "\x1B[A",
         ## down | left | right | up
         'volume-down': "9", 'mute': "m", 'volume-up': "0",
         'stop': "q", 'pause': " ", 'play': " "},
    'omxplayer':
        {'rewind-big': "\x1B[B", 'rewind': "\x1B[D", 'ff': "\x1B[C", 'ff-big': "\x1B[A",
         'volume-down': "+", 'mute': " ", #omxplayer doesn't have a mute, so we pause instead
         'volume-up': "-", 
         'stop': "q", 'pause': " ", 'play': " "}
    }


player = {
    'audio': ["mplayer"],
    'video': []
    }

try:
    call(["omxplayer"])
    player['video'] = ["omxplayer"]
except:
    player['video'] = ["mplayer", "-fs"]

currentPlayer = False

This is a bunch of starting state. ext maps various extensions to either audio or video files, which is relevant both for the presentation layer[8] and the back-end[9]. root is a list of directories to start in, and ideally, there should be security checks that any file we play/directory we show is contained in one of these. I have made a second note of it.

commands is the table that our command handler looks values up in. They're mappings between expected commands from the front-end to values that our player programs will understand. They're similar for the most part, but omx doesn't have mute and uses +/- to manipulate volume, where mplayer uses 9/0. The idea is that if you look up a command in these tables, the result you'll get is a string you can write to the player stream in order to get it to respond to that command.

player is a mapping of file-type to player command. It always uses mplayer for audio[10], but checks for the presence of omxplayer with that try/except block before deciding to use it for videos. If it doesn't find omxplayer[11], it uses mplayer in full-screen mode instead.

currentPlayer is a hook to the current player process. It's False if there isn't a player running, and a (name, process) tuple if there is one[12].

Moving on to the last piece:

import os, json
import conf

def isExt(filename, extList):
    name, ext = os.path.splitext(filename)
    if ext[1:] in extList:
        return True
    return False

def isAudio(filename):
    return isExt(filename, conf.ext['audio'])

def isVideo(filename):
    return isExt(filename, conf.ext['video'])

def typeOfFile(path):
    if isAudio(path):
        return 'audio'
    elif isVideo(path):
        return 'video'
    else:
        raise LookupError("can't decide filetype of '%s'" % [path])

def nameToTitle(filename):
    return re.sub(" [ ]+", " - ", re.sub("-", " ", os.path.basename(filename).title()))

def entryToJSON(entry):
    name, ext = os.path.splitext(entry)
    if ext == '':
        ext = "directory"
    else:
        ext = ext[1:]
    return {'path': entry, 'type': ext, 'name': nameToTitle(name), 'buttons': True}

def entriesToDicts(entries):
    dirs, videos, music = [[],[],[]]
    for f in entries:
        res = entryToJSON(f)
        if os.path.isdir(res['path']):
            dirs.append(res)
        elif res['type'] in conf.ext['video']:
            videos.append(res)
        elif res['type'] in conf.ext['audio']:
            music.append(res)
    return dirs + videos + music

def entriesToJSON(entries):
    return json.dumps(entriesToDicts(entries))

def dirToJSON(directory):
    entries = entriesToDicts(
        map(lambda p: os.path.join(directory, p), 
            sorted(os.listdir(directory))))
    if directory in conf.root:
        entries.insert(0, {'path': "root", 'name': "..", 'type': "directory"})
    else:
        entries.insert(0, {'path': os.path.dirname(directory), 'name': "..", 'type': "directory"})
    return json.dumps(entries)

That ... seems pretty self-explanatory, actually. The file predicates at the top figure out what's what based on conf.py data. The last few functions there handle the conversion of directories and directory entries to JSON objects that can easily be fed to the front-end. This is where you'll see why I wanted a buttons option in the data itself by the way; some JSON dumps include an entry that lets the user navigate to the previous directory, and we don't really want a play or shuffle option on those. nameToTitle takes a filename and returns the corresponding display title based on my own idiosyncratic naming convention[13].

There's a few things that this player obviously still needs. I have to put together some functions that let me check whether input to the /list-directory and /play handlers represents allowed files and not, say, /dev/secret-files/. That's a security concern, and I mentioned it as a downside to the approach when I first wrote about the JS MVC frameworks. Basically, if your front-end is entirely separate from your back-end, you can't treat it as trusted code[16]. You need to assume that malicious packets are going to come in through your external handlers, and you need to deal with them appropriately.

Other than that, features I'll be building over the next little while include

  • playing directories and lists of files[17]
  • playlist management[18]
  • better volume and seek control[19]
  • ability to send HDMI events to the output[20]

but they're all icing, as far as I'm concerned. This is now a pretty decent, working web-interface for a media server on the RasPi written in 389 lines of Python/JS/HTML/CSS. Once again, the github has been updated[21] if you want to poke around with it.

Now if you'll excuse me, I'm going to spend a couple of hours putting it to good use.


Footnotes

1 - [back] - And, as you'll see, one is still a no-op.

2 - [back] - Where Hunchentoot runs in single-threaded mode, but that's not a big deal for an application like this.

3 - [back] - You could be forgiven for thinking this is trivial if you haven't done it before.

4 - [back] - Defaulting to something called conf.root.

5 - [back] - No location checking yet, I'm making a note to add that later.

6 - [back] - There should only ever one player active, since the point of this server is to control one display.

7 - [back] - We're not using the communicate method since that closes the target stream and we want it to stay open.

8 - [back] - Because we display different icons for videos than for music files.

9 - [back] - Because we potentially use a different player for audio and video files.

10 - [back] - Though I guess I could figure out what the default RasPi audio player is and use that instead.

11 - [back] - Which means it's not running on a RasPi.

12 - [back] - As an aside, this is the source of a pretty horrible heisenbug I ran into. You see, web.py isn't fully interpreted; when you run it, it starts a process that watches relevant files and re-compiles them if they change. Sounds ok, but because of that global hook assigning currentPlayer to False, whenever I made a change to the containing file, it would reset without terminating the current player. I spent a fun half hour or so trying to figure out what the hell was going on when it occurred to me that my development environment was leaving floating processes lying around. I'm not entirely sure conf.py is the best place to keep that start-up variable, since it's the one most likely to change at runtime, but I honestly don't know how to solve the higher problem in a general way

13 - [back] - Bonus Points [14] if you can figure it out based on that pair of regex substitutions.

14 - [back] - Bonus Points can be redeemed for Regular Points [15].

15 - [back] - Regular Points can be redeemed for nothing.

16 - [back] - Which is one reason that I'm glad Python's subprocess.Popen takes a list and appropriately escapes the contents rather than taking a string and leaving shell-injection vectors as so many other languages opt to.

17 - [back] - Rather than just single files.

18 - [back] - Probably as an entirely front-end construct, but we'll see.

19 - [back] - Ideally, both would be sliders, but I went with buttons for the first pass because synchronizing state to the extent proper sliders would require seems rickety and error-prone.

20 - [back] - This is for TV control; ideally, I'd be able to turn it on, change channels and control actual output volume from the same web interface that tells mplayer and omxplayer what to do.

21 - [back] - Oddly, it lists this as a JavaScript project with code contents Common Lisp: 100%, rather than the mixture of Python, JS and HTML/CSS that it currently is.

Saturday, October 6, 2012

WebMote the Right Way™©

A little while ago, I mentioned that while the new wave of JS frameworks I observed were shit overall, they encouraged the correct approach to web UI building. Well, since I'll have to do a pretty serious re-write of WebMote for use with the RasPi, I figured it would be a good time to apply the principle. This is going to be a two-parter; firstly because I really want to focus on the front-end today, secondly because I don't have the back-end done yet, thirdly because I'm planning to some pretty odd things or the server-side of this project, and fourthly because I'm trying not to bore the ever-loving fuck out of everyone reading this.

So.

First off, have a tree

web-mote
├── css
│   ├── custom-theme ## jQueryUI CSS and images
│   ├── icons ## the SILK icon set
│   ├── style.css
│   └── watermark
│       ├── audio.png
│       ├── folder.png
│       ├── image.png
│       └── video.png
├── js
│   ├── backbone-min.js
│   ├── handlebars-1.0.rc.1.js
│   ├── jquery.min.js
│   ├── jquery-ui-1.8.13.custom.min.js
│   ├── underscore-min.js
│   └── webmote.js
├── root-directory
└── webmote.html

Most of that is framework code, of course. The only files I'll be going through today are webmote.html, webmote.css and webmote.js. You'll note the presence of backbone and underscore; I use their Router class, but don't otherwise touch them for reasons.

<!DOCTYPE HTML>
<html lang="en-US">
  <head>
    <meta charset="UTF-8">
    <title>WebMote</title>
  </head>
  <body>

    <!-- --------- -->
    <!-- Templates -->
    <!-- --------- -->
    <script id="tmp-folder" type="text/x-handlebars-template">
      <li class="{{type}}">
        <button class="play icon play" onclick="mote.playDir('{{path}}')"></button>
        <button class="shuffle icon shuffle" onclick="mote.shuffleDir('{{path}}')"></button>
        <a class="dir-link" href="#navigate{{path}}">{{name}}</a>
      </li>
    </script>

    <script id="tmp-file" type="text/x-handlebars-template">
      <li class="{{type}}">
        <a class="file-link" href="javascript:void(0);" onclick="mote.play('{{path}}')">{{name}}</a>
      </li>
    </script>

    <script id="tmp-control" type="text/x-handlebars-template">
      <li>
        <button class="icon {{cmd}}{{#if css-class}} {{css-class}}{{/if}}" onclick="mote.command('{{cmd}}');">
        </button>
      </li>
    </script>

    <script id="tmp-control-block" type="text/x-handlebars-template">
      <ul>
        {{#each this}}
        {{#control-button this}}{{/control-button}}
        {{/each}}
      </ul>
    </script>
    
    <!-- ---- -->
    <!-- Body -->
    <!-- ---- -->
    <ul id="file-list"></ul>
    <div id="controls"></div>
    
    <!-- ------ -->
    <!-- Styles -->
    <!-- ------ -->
    <link rel="stylesheet" href="css/style.css" type="text/css" media="screen" />
    <link rel="stylesheet" href="css/custom-theme/jquery-ui-1.8.13.custom.css" type="text/css" media="screen" />
    
    <!-- ------- -->
    <!-- Scripts -->
    <!-- ------- -->
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <script type="text/javascript" src="js/jquery-ui-1.8.13.custom.min.js"></script>
    <script type="text/javascript" src="js/handlebars-1.0.rc.1.js"></script>
    <script type="text/javascript" src="js/underscore-min.js"></script>
    <script type="text/javascript" src="js/backbone-min.js"></script>

    <script type="text/javascript" src="js/webmote.js"></script>

  </body>
</html>

That's actually the entire front-end markup. If you haven't seen HTML before, it might look daunting but it's extremely simple. You've got the head up top adding a little bit of metadata, all the text/javascript and text/css includes at the bottom, and only two actual elements in the body of the page: placeholders to pump a file list and control buttons into later. The interesting part is those four handlebars templates.

    <!-- --------- -->
    <!-- Templates -->
    <!-- --------- -->
    <script id="tmp-folder" type="text/x-handlebars-template">
      <li class="{{type}}">
        <button class="play icon play" onclick="mote.playDir('{{path}}')"></button>
        <button class="shuffle icon shuffle" onclick="mote.shuffleDir('{{path}}')"></button>
        <a class="dir-link" href="#navigate{{path}}">{{name}}</a>
      </li>
    </script>

    <script id="tmp-file" type="text/x-handlebars-template">
      <li class="{{type}}">
        <a class="file-link" href="javascript:void(0);" onclick="mote.play('{{path}}')">{{name}}</a>
      </li>
    </script>

    <script id="tmp-control" type="text/x-handlebars-template">
      <li>
        <button class="icon {{cmd}}{{#if css-class}} {{css-class}}{{/if}}" onclick="mote.command('{{cmd}}');">
        </button>
      </li>
    </script>

    <script id="tmp-control-block" type="text/x-handlebars-template">
      <ul>
        {{#each this}}
        {{#control-button this}}{{/control-button}}
        {{/each}}
      </ul>
    </script>

Firstly, notice that they're all in script type="text/x-handlebars-template" tags. I think this technically goes against some markup standard, so it may get screamed at by an XHTML validator somewhere, but it's not a huge deal. If you really feel that's something you want to fix, you can also put these templates in hidden divs instead of script tags; as you'll see later, it wouldn't make a difference in how we use them.

Also, note that I'm inlining the onclick events rather than dealing with them later. A while ago, I sort of got used to the idea of just putting ids and classes on pieces of the DOM tree, then using jQuery to apply events later with $("#foo").click(function () { thingsThatShouldHappen(); });. It looks cleaner at first glance because it separates the presentation and behavior of your controls, but there are two pretty big problems with the approach. First, it requires additional DOM traversals. Those may or may not get optimized away in modern JS engines, but I'm not entirely sure. Second, and more importantly, it makes it harder to change the layout at runtime later, and that's something you'll want to do fairly frequently if you're looking to make a responsive application.

That's actually why I ended up separating tmp-control out of tmp-control-block. I honestly wasn't even going to generate those programatically, but I ran into a situation where I wanted to replace one button with another under certain conditions. I'll point it out when I get to it; the short version is, had I stuck to $ing click events, I would have had to run that setup function each time the change happened[1]. Having those events inlined lets me do the simple thing of just re-rendering a template at the appointed place.

Back to the code at hand.

    <script id="tmp-folder" type="text/x-handlebars-template">
      <li class="{{type}}">
        <button class="play icon play" onclick="mote.playDir('{{path}}')"></button>
        <button class="shuffle icon shuffle" onclick="mote.shuffleDir('{{path}}')"></button>
        <a class="dir-link" href="#navigate{{path}}">{{name}}</a>
      </li>
    </script>

The interesting part of the templates are those snippets of code in the titular handlebars that look something like {{foo}} or {{#foo}}{{/foo}}. A template expects an object, or possibly a list as its input later, and these snippets act as lookups. So, for example, where it says {{path}}, that particular template will look up the value "path" in its argument and echo the result. Now that you know that, the first two templates are fairly self-explanatory.

The last two could use some close-up time though.

    <script id="tmp-control" type="text/x-handlebars-template">
      <li>
        <button class="icon {{cmd}}{{#if css-class}} {{css-class}}{{/if}}" onclick="mote.command('{{cmd}}');">
        </button>
      </li>
    </script>

The single control template demonstrates an #if block. When you see the construct {{#if foo}}{{bar}}{{/if}} in a template, what happening is argument.bar is echoed if argument.foo is not one of "", false, null, [] or undefined.

    <script id="tmp-control-block" type="text/x-handlebars-template">
      <ul>
        {{#each this}}
        {{#control-button this}}{{/control-button}}
        {{/each}}
      </ul>
    </script>

The control-block is responsible for outputting a group of controls. this in a handlebars block refers to the current argument, and the {{#each foo}}...{{/each}} construct iterates through a list. It doesn't seem like there's a good way of iterating over k/v pairs built in, but as that #control-button block demonstrates, it's possible to define your own helpers (spoiler, the control-button helper just renders an individual control).

More on that later though, let me take a very quick look at the CSS before we move on to the JS.

/** The Control Panel *******************************************/
#controls { text-align: center; width: 100%; position: fixed; bottom: 0px; padding: 5px 0px; background-color: rgba(255, 255, 255, 0.8); }
#controls ul { list-style-type: none; margin: 0px; margin-bottom: 3px; }
#controls li { display: inline; }

/** The Main File List ******************************************/
#file-list { list-style-type: none; width: 350px; margin: auto; }
#file-list li { margin-bottom: 10px; padding: 3px; clear: both; }
#file-list li button { margin-top: 0px; float: left; }
#file-list li a { min-height: 20px; padding: 4px; float: left; border: 1px solid #ddd; border-radius: 5px; background: right no-repeat; background-size: 50px; }
#file-list li a.dir-link { white-space: nowrap; width: 228px; }
#file-list li a.file-link { word-break: break-all; width: 320px; }

#file-list li.directory a { background-image: url(watermark/folder.png); }
#file-list li.mp4 a, #file-list li.mov a, #file-list li.ogv a { background-image: url(watermark/video.png); }
#file-list li.mp3 a, #file-list li.ogg a { background-image: url(watermark/audio.png); }

/** BASICS ******************************************************/
h1, h2, h3, h4, h5, h6 { margin: 0px; padding: 0px; }

body { margin-bottom: 130px; }

.pointer { cursor: pointer; }
.hidden { display: none; }
.clear { clear: both; }

/** BUTTONS, MOTHERFUCKER, DO YOU USE THEM?! ********************/
button { background: no-repeat 3px 3px; margin: 3px;  height: 28px; width: 40px; background-color: #ddd; border-radius: 8px; cursor: pointer; text-align: left; }
button.icon { text-indent: 15px; }
button.big { width: 90px; }

/***** specific button types ************************************/
button.volume-up { background-image: url(icons/sound.png); }
button.volume-down { background-image: url(icons/sound_low.png); }
button.mute { background-image: url(icons/sound_mute.png); }

button.rewind-big { background-image: url(icons/resultset_previous.png); }
button.rewind { background-image: url(icons/control_rewind_blue.png); }
button.stop { background-image: url(icons/control_stop_blue.png); }
button.pause { background-image: url(icons/control_pause_blue.png); }
button.play { background-image: url(icons/control_play_blue.png); }
button.ff { background-image: url(icons/control_fastforward_blue.png); }
button.ff-big { background-image: url(icons/resultset_next.png); }
button.shuffle { background-image: url(icons/arrow_switch.png); }

There isn't a lot to explain here. The button section sets up nice, clickable buttons with those icons I mentioned earlier, the basics provide some utility classes. The Control Panel and Main File List sections should give you a pretty good idea of what the final UI is going to look like, and what those watermark images are for.

Ok, now that we've taken that short, stylish break, onto the meat.

var util = {
    requestJSON : function (url, dat, type) {
        var res = null;
        $.ajax({
            url: url,
            data: dat,
            type: type,
            success: function (data) { res = $.parseJSON(data); },
            async: false
        });
        return res;
    },
    getJSON : function (url, dat) { return util.requestJSON(url, dat, "GET"); },
    postJSON : function (url, dat) { return util.requestJSON(url, dat, "POST"); }
};

var mote = {
    targetId: "#file-list",
    render: function (fileList) {
        if (fileList) {
            $(mote.targetId).empty();
            $.each(fileList,
                   function (index, e){
                       if (e.type == "directory") 
                           $(mote.targetId).append(templates.folder(e))
                       else 
                           $(mote.targetId).append(templates.file(e))
                   })
                }
    },
    renderButton: function (control) {
        
    },
    renderControls: function (controlLists) {
        $.each(controlLists,
               function (index, e) {
                   $("#controls").append(templates.controlBlock(e));
               })
            },
    play: function (file) {
        console.log(["cmd", "play", file]);
        $.post("/play",
               {"file" : file},
               function (data, textStatus) { 
                   console.log(["now playing", file, textStatus]);
               });
    },
    playDir: function (dir) {
        console.log(["cmd", "play", "directory", dir]);
        $.post("/play-directory", {"dir": dir});
    },
    shuffleDir: function (dir) {
        console.log(["SHUFFLE", dir]);
        $.post("/shuffle-directory", {"dir": dir});
    },
    command: function (cmd) {
        console.log(cmd);
        $.post("/command", {"command": cmd},
               function () {
                   if (cmd == "pause") {
                       var btn = templates.control({cmd: "play", "css-class": "big"});
                       $("#controls button.pause").replaceWith(btn);
                   } else if (cmd == "play") {
                       var btn = templates.control({cmd: "pause", "css-class": "big"});
                       $("#controls button.play").replaceWith(btn);
                   }
               })
    },
    navigate: function (dir) {
        console.log(["cmd", "display", dir]);
        mote.render(util.getJSON("/show-directory", {"dir": dir}));
    }
}

Handlebars.registerHelper("control-button", function (ctrl) {
    return new Handlebars.SafeString(templates.control(ctrl));
});

var templates = {
    folder : Handlebars.compile($("#tmp-folder").html()),
    file : Handlebars.compile($("#tmp-file").html()),
    control: Handlebars.compile($("#tmp-control").html()),
    controlBlock : Handlebars.compile($("#tmp-control-block").html())
}

var Routes = Backbone.Router.extend({ 
    routes: {
        "navigate*path": "nav"
    },
    nav: mote.navigate
});


$(document).ready(function() {
    mote.renderControls([[{cmd: "rewind-big"}, {cmd: "rewind"}, {cmd: "ff"}, {cmd: "ff-big"}],
                         [{cmd: "volume-down"}, {cmd: "mute"}, {cmd: "volume-up"}],
                         [{cmd: "stop", "css-class": "big"}, {cmd: "pause", "css-class": "big"}]]);
    mote.render(util.getJSON("/root-directory"));

    new Routes();
    Backbone.history.start();
});

I'm mostly using objects as namespaces here, and find myself wishing[2] that JavaScript let me express that more clearly. Ahem. Lets get the quick stuff out of the way first. util is the utility namespace, and contains three helper functions to let me pull JSON data from the server more easily than I could by default. I'm following the functional paradigm and having them return their results rather than depend on a callback to do work.

A bit further down,

Handlebars.registerHelper("control-button", function (ctrl) {
    return new Handlebars.SafeString(templates.control(ctrl));
});

var templates = {
    folder : Handlebars.compile($("#tmp-folder").html()),
    file : Handlebars.compile($("#tmp-file").html()),
    control: Handlebars.compile($("#tmp-control").html()),
    controlBlock : Handlebars.compile($("#tmp-control-block").html())
}

is the handlebars-related code. Handlebars.registerHelper is what makes the template helper function from earlier work properly. Note the new Handlebars.SafeString in that return line, by the way; handlebars templates escape their inputs by default, so passing a plain string won't quite do what you'd want it to in this situation. templates is the namespace in which we keep individual compiled templates. I mean ... templates. Notice that we're just identifying a DOM element by id, and running a dump of its .html() through the compiler. This is what I meant when I said that hidden divs would work just as well as scripts. Your templates can be stored in any DOM element[3], as long as you can reference it when your JS files load. Incidentally, that's why all the script includes in our HTML are at near the bottom of the structure, conveniently after our templates are defined.

Below that, and intruding slightly into the .ready() method is our in-page Router.

var Routes = Backbone.Router.extend({ 
    routes: {
        "navigate*path": "nav"
    },
    nav: mote.navigate
});


$(document).ready(function() {
    mote.renderControls([[{cmd: "rewind-big"}, {cmd: "rewind"}, {cmd: "ff"}, {cmd: "ff-big"}],
                         [{cmd: "volume-down"}, {cmd: "mute"}, {cmd: "volume-up"}],
                         [{cmd: "stop", "css-class": "big"}, {cmd: "pause", "css-class": "big"}]]);
    mote.render(util.getJSON("/root-directory"));

    new Routes();
    Backbone.history.start();
});

This is the entire reason I use backbone and its requirement underscore[4] in this file. The Routes object sets up routes to capture paths starting with #navigate, and calls mote.navigate if it finds one. We do this so that a user of this system will be able to save a link to a particular starting directory. That's also the reason we start the Router up after calling mote.render on the data coming out of /root-directory; that initial rendering would otherwise clobber the result our navigate call. The renderControls call displays all the assorted media buttons we'll need to acceptably control playback.

Lets take a detour before finishing up though; root-directory is for the moment just a text file with some test JSON data in it.

[{"path": "/home/inaimathi/videos/a-show", 
  "type": "directory", "name": "a-show"}, 
 {"path": "/home/inaimathi/videos/friendship-is-magic", 
  "type": "directory", "name": "friendship-is-magic"}, 
 {"path": "/home/inaimathi/videos/khan-academy", 
  "type": "directory", "name": "khan-academy"}, 
 {"path": "/home/inaimathi/videos/porn", 
  "type": "directory", "name": "porn"}, 
 {"path": "/home/inaimathi/videos/bizarre-porn", 
  "type": "directory", "name": "bizarre porn"}, 
 {"path": "/home/inaimathi/videos/horrible-porn", 
  "type": "directory", "name": "horrible porn"}, 
 {"path": "/home/inaimathi/videos/unforgivable-porn", 
  "type": "directory", "name": "unforgivable porn"}, 
 {"path": "/home/inaimathi/videos/Clojure-for-Lisp-Programmers-Part-1.mov", 
  "type": "mov", "name": "Clojure-for-Lisp-Programmers-Part-1.mov"}, 
 {"path": "/home/inaimathi/videos/Clojure-for-Lisp-Programmers-Part-2.mov", 
  "type": "mov", "name": "Clojure-for-Lisp-Programmers-Part-2.mov"}, 
 {"path": "/home/inaimathi/videos/Eben-Moglen--Why-Freedom-of-Thought-Requires-Free-Media-and-Why-Free-Media-Require-Free-Technology.mp4", 
  "type": "mp4", "name": "Eben-Moglen--Why-Freedom-of-Thought-Requires-Free-Media-and-Why-Free-Media-Require-Free-Technology.mp4"}, 
 {"path": "/home/inaimathi/videos/Epic-Wub-Time--Musicians-of-Ponyville.mp4", 
  "type": "mp4", "name": "Epic-Wub-Time--Musicians-of-Ponyville.mp4"}, 
 {"path": "/home/inaimathi/videos/Project-Glass--Live-Demo-At-Google-I-O.mp4", 
  "type": "mp4", "name": "Project-Glass--Live-Demo-At-Google-I-O.mp4"}, 
 {"path": "/home/inaimathi/videos/in-the-fall-of-gravity.mp4", 
  "type": "mp4", "name": "in-the-fall-of-gravity.mp4"}, 
 {"path": "/home/inaimathi/videos/beethoven-symphony-no-9.mp3", 
  "type": "mp3", "name": "beethoven-symphony-no-9.mp3"},
 {"path": "/home/inaimathi/videos/first-lsdj.ogg", 
  "type": "ogg", "name": "first-lsdj.ogg"}]

It's a list of files and directories that a server could reasonably put together and transmit out to the front end just by mapping over the output of listdir or similar. The front-end will expect data represented in roughly this format in order to display a list of files.

Now that we've got an idea of what the data looks like, the last piece of this system can fall into place.

var mote = {
    targetId: "#file-list",
    render: function (fileList) {
        if (fileList) {
            $(mote.targetId).empty();
            $.each(fileList,
                   function (index, e){
                       if (e.type == "directory") 
                           $(mote.targetId).append(templates.folder(e))
                       else 
                           $(mote.targetId).append(templates.file(e))
                   })
                }
    },
    renderControls: function (controlLists) {
        $.each(controlLists,
               function (index, e) {
                   $("#controls").append(templates.controlBlock(e));
               })
            },
    play: function (file) {
        console.log(["cmd", "play", file]);
        $.post("/play",
               {"file" : file},
               function (data, textStatus) { 
                   console.log(["now playing", file, textStatus]);
               });
    },
    playDir: function (dir) {
        console.log(["cmd", "play", "directory", dir]);
        $.post("/play-directory", {"dir": dir});
    },
    shuffleDir: function (dir) {
        console.log(["SHUFFLE", dir]);
        $.post("/shuffle-directory", {"dir": dir});
    },
    command: function (cmd) {
        console.log(cmd);
        $.post("/command", {"command": cmd},
               function () {
                   if (cmd == "pause") {
                       var btn = templates.control({cmd: "play", "css-class": "big"});
                       $("#controls button.pause").replaceWith(btn);
                   } else if (cmd == "play") {
                       var btn = templates.control({cmd: "pause", "css-class": "big"});
                       $("#controls button.play").replaceWith(btn);
                   }
               })
    },
    navigate: function (dir) {
        console.log(["cmd", "display", dir]);
        mote.render(util.getJSON("/show-directory", {"dir": dir}));
    }
}

The mote namespace contains all the relevant navigation commands that we'll need[5]. The render functions do exactly what you'd think. render clears the current file list, takes a list of file and directory objects, and runs them through the file or folder template as appropriate, appending the result to the file list. renderControls takes a tree of control objects and runs them through the controlBlock template, which in turn runs each through the control template and wraps the results in a ul.

The various play/shuffle functions pass file or directory names to whatever back-end process we'll be running this on top of. The command function is the thing that gets called by each control button. For the most part, it just passes that fact along to the back-end system and calls it a day, but there's additional logic for of pause and play signals. In that case, it also switches out the oause button for the play button, cycling between the two on every successful invocation. This is the difficulty I mentioned earlier, and it could still use a helper function or two[6]. Consider how you would write that code

  • using $("#controls button").click() instead of inlined onclick events.
  • without a separate control template.
  • without using templates at all (handlebars or otherwise).

The last part is the navigate function. It uses util.getJSON to request a new set of folders/files and renders them.

And that's it.

This complete front-end, including the CSS weighs in at just over 200 lines of code. It doesn't do anything yet, but that's just because we haven't slapped it in front of an application server. Note that it will be completely, entirely separate from whatever back-end we end up using. It'll work as long as said back-end supports the appropriate requests, and returns the correct data from /show-directory and /root-directory. In fact, if you'd like to make a compatible back-end, all you need to do is provide the following:

  • /root-directory
  • /show-directory (we could probably get rid of the previous handler by letting this one take a zero-parameter request)
  • /play
  • /play-directory (if you wanted to provide a polymorphic play handler, you could do without this one)
  • /shuffle-directory
  • /command

That's slightly misleading because the command handler needs to respond to 10 different parameters, depending on what button the user presses, but it's still much simpler than re-writing the front end for each new back-end. This cuts both ways too; someone wanting to implement their own front-end to the corresponding server needs to implement as many of those handlers as they'll need, and they're good to go.

Keep in mind that this is half the total goal. Next time, we'll see exactly how simple the server-side can get when we completely remove user interface concerns from it. Anyway, thus endeth the lesson. I'm not updating the github repo until I get around to a complete RasPi-compatible media server, but the above code should be easy enough to play with.


Footnotes

1 - [back] - It would actually have been even more complicated than that, because it wouldn't have been enough to naively run $("button").click(...) again. That would put duplicate events on every button that wasn't replaced. I'd have gotten into the situation of either targeting just the new button separately, incurring code duplication, or writing the setup function in such a way that it unbound the click event for all buttons first, then re-bound them. That's ... inelegant.

2 - [back] - Not for the first time.

3 - [back] - Or even pre-compiled, if you feel like mucking about with node.js for a bit.

4 - [back] - To be fair, I guess, underscore.js also has a half-way decent looking templating system itself. It looks minimal, but flexible, and I'll be playing around with it a bit after this write up in an effort to potentially boot handlebars out of the include tree.

5 - [back] - console.log calls have been left in for testing purposes, in case you want to take this pure front-end for a spin, by the way. It actually works if you have a reasonably recent browser.

6 - [back] - Which I'll put in if it turns out that more than one button needs this kind of behavior.