diff --git a/client/djrandom_client/djfuse.py b/client/djrandom_client/djfuse.py
new file mode 100644
index 0000000000000000000000000000000000000000..e77556b536fc9b1de1adbe8c007c99b9b529f532
--- /dev/null
+++ b/client/djrandom_client/djfuse.py
@@ -0,0 +1,141 @@
+import json
+import shelve
+import shutil
+import urllib2
+from errno import EINVAL, ENOENT
+from stat import S_IFDIR, S_IFREG
+from fuse import FUSE, FuseOSError, Operations, LoggingMixIn
+
+
+class DJFS(LoggingMixIn, Operations):
+    """Simple FUSE client for djrandom.
+
+    Quite inefficient, but it shows that it can be done easily
+    with the existing API, accessing files using the standard
+    /artist/album/title hierarchy.
+    """
+
+    def __init__(self, server_url, cache_dir):
+        self._server_url = server_url.rstrip('/')
+        self._cache_dir = cache_dir
+        self._opener = urllib2.build_opener()
+        self._hash_cache = shelve.open(
+            os.path.joib(cache_dir, 'artist_album'))
+
+    def _get(self, url):
+        req = urllib2.Request(self._server_url + url)
+        return json.load(self._opener.open(req))
+
+    @classmethod
+    def _hash_cache_key(cls, artist, album, title):
+        return '%s|%s|%s' % (artist, album, title)
+
+    def _load_into_cache(self, artist, album, songs):
+        for song in songs:
+            key = self._hash_cache_key(artist, album, song['title'])
+            self._hash_cache[key] = song['sha1']
+
+    def _trigger_cache_load(self, artist, album):
+        contents = self._get('/json/album/%s/%s' % (artist, album))['songs']
+        self._load_into_cache(artist, album, contents)        
+
+    def _find_sha1(self, artist, album, title):
+        key = self._hash_cache_key(artist, album, title)
+        sha1 = self._hash_cache.get(key)
+        if not sha1:
+            self._trigger_cache_load(artist, album)
+            sha1 = self._hash_cache.get(key)
+        return sha1
+
+    def _in_file_cache(self, sha1):
+        path = os.path.join(self._cache_dir, sha1[0], sha1)
+        return os.path.exists(path)
+
+    def _download_file(self, sha1):
+        path = os.path.join(self._cache_dir, sha1[0], sha1)
+        req = urllib2.Request('/dl/%s' % sha1)
+        resp = self._opener.open(req)
+        with open(path, 'wb') as fd:
+            shutil.copyfileobj(req, fd)
+
+    def _parse_path(self, path):
+        parts = path.split('/')
+        if len(parts) > 3:
+            raise FuseOSError(EINVAL)
+        if len(parts) < 3:
+            parts.extend([None for x in (3 - len(parts))])
+        return parts
+
+    def getattr(self, path, fh=None):
+        artist, album, title = self._parse_path(path)
+        if title:
+            sha1 = self._cache_get(artist, album, title)
+            if not sha1:
+                raise FuseOSError(ENOENT)
+            stats = self._get('/json/song/%s' % sha1)
+            mtime = float(stats['uploaded_at'])
+            return dict(st_mode=(S_IFREG | 0444), st_nlink=1,
+                        st_size=int(stats['size']),
+                        st_ctime=mtime,
+                        st_mtime=mtime,
+                        st_atime=mtime)
+        else:
+            return dict(st_mode=(S_IFDIR | 0555), st_nlink=2,
+                        st_size=0, st_ctime=0, st_mtime=0, st_atime=0)
+
+    def readdir(self, path, fh=None):
+        artist, album, title = self._parse_path(path)
+        if title:
+            raise FuseOSError(EINVAL)
+        if not artist:
+            values = self._get('/json/artists')['artists']
+        elif not album:
+            values = self._get('/json/albums/%s' % artist)['albums']
+        else:
+            # equal to trigger_cache_load()
+            contents = self._get('/json/album/%s/%s' % (artist, album))['songs']
+            self._load_into_cache(artist, album, contents)
+            values = [x['sha1'] for x in contents]
+        return ['.', '..'] + [x for x in values]
+
+    def read(self, path, size, offset, fh=None):
+        if not self._in_file_cache(path):
+            self._download_file(path)
+        with open(path, 'rb') as fd:
+            fd.seek(offset)
+            return fd.read(size)
+
+    # Disable unsupported calls.
+    chmod = None
+    chown = None
+    create = None
+    rename = None
+    rmdir = None
+    statfs = None
+    symlink = None
+    truncate = None
+    unlink = None
+    utimens = None
+    write = None
+    listxattr = None
+    removexattr = None
+    setxattr = None
+
+
+def main():
+    import optparse
+    parser = optparse.OptionParser(usage='%Prog <MOUNTPOINT>')
+    parser.add_option('--api_key')
+    parser.add_option('--cache_dir',
+                      default='/var/cache/djrandom/fuse')
+    parser.add_option('--server_url',
+                      defaults='http://djrandom.incal.net')
+    opts, args = parser.parse_args()
+    if len(args) != 1:
+        parser.error('Wrong number of args')
+
+    fuse = FUSE(DJFS(), args[0], foreground=True)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/server/djrandom/frontend/frontend.py b/server/djrandom/frontend/frontend.py
index 0f8f7210950501eb3e3f254c788e0eeb3b29a16e..da85c63943c6078c7095e3a1c53ac1e046ff9e8e 100644
--- a/server/djrandom/frontend/frontend.py
+++ b/server/djrandom/frontend/frontend.py
@@ -82,7 +82,10 @@ def song_info_json(sha1):
     mp3 = MP3.query.get(sha1)
     if not mp3:
         abort(404)
-    return jsonify(mp3.to_dict())
+    # Use the mp3.to_dict() data, but add some more things...
+    data = mp3.to_dict()
+    data['size'] = os.path.getsize(mp3.path)
+    return jsonify(data)
 
 
 @app.route('/json/playing', methods=['POST'])
diff --git a/server/djrandom/model/mp3.py b/server/djrandom/model/mp3.py
index 8ec8b4851220f2440ef0996ec254557781406931..9526c01ba215c51ddd1cdf16718841ca58ebf55f 100644
--- a/server/djrandom/model/mp3.py
+++ b/server/djrandom/model/mp3.py
@@ -31,7 +31,8 @@ class MP3(Base):
                 'artist': self.artist,
                 'album': self.album,
                 'genre': self.genre,
-                'sha1': self.sha1}
+                'sha1': self.sha1,
+                'uploaded_at': self.uploaded_at}
 
 
 class PlayLog(Base):