diff --git a/server/djrandom/frontend/api_views.py b/server/djrandom/frontend/api_views.py
index b8a8dbbb7bb47d3b9f73da88c8c5e3eee5dba7b3..e4cf6aec432a7cfe9466bd68738c235d354ef530 100644
--- a/server/djrandom/frontend/api_views.py
+++ b/server/djrandom/frontend/api_views.py
@@ -165,11 +165,19 @@ def last_uploaded_json():
     return jsonify(results=last_uploaded)
 
 
-@app.route('/json/random', methods=['POST'])
+@app.route('/json/markov', methods=['POST'])
 @require_auth
-def random_json():
+def markov_json():
     n = int(request.form.get('n', 10))
     hashes = request.form.get('h', '').split(',')
     last_song = hashes[-1]
     sequence = svcs['markov'].generate_sequence(last_song, 2, n)
     return jsonify(results=sequence)
+
+
+@app.route('/json/random', methods=['GET'])
+@require_auth
+def random_json():
+    n = int(request.args.get('n', 10))
+    random_songs = [x.sha1 for x in MP3.get_random_songs(n)]
+    return jsonify(results=random_songs)
diff --git a/server/djrandom/frontend/static/js/djr.min.js b/server/djrandom/frontend/static/js/djr.min.js
index ab4ad99916aaf9e15ce36378cb4d45f69f642ee2..9b3bb309c3dab906692d8d464fa17ba46f74f02c 100644
--- a/server/djrandom/frontend/static/js/djr.min.js
+++ b/server/djrandom/frontend/static/js/djr.min.js
@@ -1,7 +1,7 @@
 djr={};var CHARS="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split("");djr.generateRandomId=function(){var a=[],b=CHARS,c,d=b.length;for(c=0;c<40;c++)a[c]=b[0|Math.random()*d];return a.join("")};djr.Backend=function(){};djr.Backend.prototype.search=function(a,b,c){$.ajax({url:"/json/search",data:{q:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.moreLikeThese=function(a,b,c){$.ajax({url:"/json/morelikethese",data:{h:a.join(",")},dataType:"json",type:"POST",context:c||this,success:function(a){b(a.results)}})};
 djr.Backend.prototype.getPlaylist=function(a,b,c){$.ajax({url:"/json/playlist/get/"+a,dataType:"json",type:"GET",context:c||this,success:function(c){b(new djr.PlaylistChunk(c.songs,a))}})};djr.Backend.prototype.savePlaylist=function(a,b){$.ajax({url:"/json/playlist/save",data:{uuid:a,h:b.join(",")},type:"POST"})};djr.Backend.prototype.streamPlaylist=function(a,b,c,d){$.ajax({url:"/json/playlist/stream",data:{uuid:a,stream:b?"y":"n"},dataType:"json",type:"POST",context:d||this,success:function(a){c(a)}})};
 djr.Backend.prototype.getHtmlForSongs=function(a,b,c){$.ajax({url:"/fragment/songs",data:{h:a.join(",")},dataType:"html",type:"POST",context:c||this,success:b})};djr.Backend.prototype.nowPlaying=function(a,b){$.ajax({url:"/json/playing",data:{cur:a,prev:b.join(",")},type:"POST"})};djr.Backend.prototype.mostPlayedPlaylist=function(a,b,c){$.ajax({url:"/json/most_played",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};
-djr.Backend.prototype.lastPlaylist=function(a,b,c){$.ajax({url:"/json/last_uploaded",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.randomPlaylist=function(a,b,c,d){$.ajax({url:"/json/random",data:{h:b.join(","),n:a},dataType:"json",type:"POST",context:d||this,success:function(a){c(a.results)}})};djr.PlaylistChunk=function(a,b){this.songs=a||[];this.title=b};djr.PlaylistChunk.prototype.hasSong=function(a){return this.songs.indexOf(a)>=0};djr.PlaylistChunk.prototype.removeSong=function(a){this.songs=$.grep(this.songs,function(b){return b!=a})};djr.controlButtons=function(a){return'<div class="ctlbox" style="display:none"><a id="'+a+'_remove" class="ctl_btn ctl_remove">&nbsp;</a></div>'};
+djr.Backend.prototype.lastPlaylist=function(a,b,c){$.ajax({url:"/json/last_uploaded",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.Backend.prototype.markovPlaylist=function(a,b,c,d){$.ajax({url:"/json/markov",data:{h:b.join(","),n:a},dataType:"json",type:"POST",context:d||this,success:function(a){c(a.results)}})};djr.Backend.prototype.randomPlaylist=function(a,b,c){$.ajax({url:"/json/random",data:{n:a},dataType:"json",type:"GET",context:c||this,success:function(a){b(a.results)}})};djr.PlaylistChunk=function(a,b){this.songs=a||[];this.title=b};djr.PlaylistChunk.prototype.hasSong=function(a){return this.songs.indexOf(a)>=0};djr.PlaylistChunk.prototype.removeSong=function(a){this.songs=$.grep(this.songs,function(b){return b!=a})};djr.controlButtons=function(a){return'<div class="ctlbox" style="display:none"><a id="'+a+'_remove" class="ctl_btn ctl_remove">&nbsp;</a></div>'};
 djr.PlaylistChunk.prototype.wrapHtml=function(a,b){return'<div id="chunk_'+a+'" class="chunk"><div class="chunk_ctl_wrap">'+djr.controlButtons("chunk_ctl_"+a)+'<a class="chunk_title">'+this.title+'</a></div><div class="chunk_inner">'+(b||"")+"</div></div>"};djr.Playlist=function(a){this.uuid=a||djr.generateRandomId();this.chunks=[];this.song_map={};this.chunk_map={};this.next_chunk_id=0};
 djr.Playlist.prototype.allSongs=function(){var a=[],b,c;for(b=0;b<this.chunks.length;b++)for(c=0;c<this.chunk_map[this.chunks[b]].songs.length;c++)a.push(this.chunk_map[this.chunks[b]].songs[c]);return a};djr.Playlist.prototype.createUniqueChunk=function(a,b){var c=[],d;for(d=0;d<a.length;d++)this.song_map[a[d]]==null&&c.push(a[d]);return c.length>0?new djr.PlaylistChunk(c,b):null};
 djr.Playlist.prototype.addChunk=function(a){djr.debug("adding chunk to playlist "+this.uuid);var b,c=this.next_chunk_id++;for(b=0;b<a.songs.length;b++)this.song_map[a.songs[b]]=c;this.chunk_map[c]=a;this.chunks.push(c);return c};djr.Playlist.prototype.getChunkSongs=function(a){return this.chunk_map[a].songs};
@@ -9,8 +9,8 @@ djr.Playlist.prototype.removeChunk=function(a){djr.debug("removing chunk "+a);va
 djr.Playlist.prototype.merge=function(){var a=[],b;for(b=0;b<this.chunks.length;b++)a.push(this.chunk_map[this.chunks[b]].title);a=a.join(" + ");b=new djr.Playlist;b.uuid=this.uuid;b.addChunk(new djr.PlaylistChunk(this.allSongs(),a));return b};djr.Playlist.prototype.getNextSong=function(a){var b=this.song_map[a],c=this.chunk_map[b].songs,b=this.chunks.indexOf(b),a=c.indexOf(a)+1;a>=c.length&&(a=0,b++,b>=this.chunks.length&&(b=0));return this.chunk_map[this.chunks[b]].songs[a]};djr.Player=function(a,b){this.backend=a;this.player=$(b);this.playlist=new djr.Playlist;this.old_songs=[];this.cur_song=null;this.player.jPlayer({swfPath:"/static/js",ready:function(){djr.debug("player ready")}});this.player.bind($.jPlayer.event.ended+".djr",function(){djr.state.player.nextSong()});this.player.bind($.jPlayer.event.error+".djr",function(){djr.state.player.reportError()})};djr.Player.prototype.hideAllChunks=function(){$(".chunk .chunk_inner").hide()};
 djr.Player.prototype.removeChunk=function(a){this.playlist.removeChunk(a);this.savePlaylist();$("#chunk_"+a).remove()};djr.Player.prototype.removeSong=function(a){$("#song_"+a).remove();a=this.playlist.removeSong(a);this.savePlaylist();a>0&&$("#chunk_"+a).remove()};djr.Player.prototype.savePlaylist=function(){this.backend.savePlaylist(this.playlist.uuid,this.playlist.allSongs())};djr.Player.prototype.clearPlaylist=function(){this.playlist=new djr.Playlist;$("#playlistDiv").empty()};
 djr.Player.prototype.mergePlaylistChunks=function(){this.playlist=this.playlist.merge();var a=[];$(".chunk .chunk_inner").each(function(){a.push($(this).html())});$("#playlistDiv").empty();var b=this.playlist.chunks[0];this.setChunkHtml(this.playlist.chunk_map[b],b,a.join(""))};djr.Player.prototype.search=function(a){var b=this;this.backend.search(a,function(c){var d=[];$.each(c,function(a,b){d.push(b.sha1)});d.length==0?djr.debug("No results found."):b.createChunk(d,a)})};
-djr.Player.prototype.lastPlaylist=function(a){var b=this,c="Last "+a+" Songs Uploaded";this.backend.lastPlaylist(a,function(a){a.length==0?djr.debug("No results found."):b.createChunk(a,c)})};djr.Player.prototype.randomPlaylist=function(a){var b=this,c="Last "+a+" Random Songs ";this.backend.randomPlaylist(a,this.playlist.allSongs(),function(a){a.length==0?djr.debug("No results found."):b.createChunk(a,c)})};
-djr.Player.prototype.mostPlayedPlaylist=function(a){var b=this,c="Most "+a+" Played Songs";this.backend.mostPlayedPlaylist(a,function(a){var e=[];$.each(a,function(a,b){e.push(b.sha1)});e.length==0?djr.debug("No results found."):b.createChunk(e,c)})};djr.Player.prototype.extendCurrentPlaylist=function(){var a=this;this.backend.moreLikeThese(this.playlist.allSongs(),function(b){a.createChunk(b,"suggestions")})};
+djr.Player.prototype.lastPlaylist=function(a){var b=this,c="Last "+a+" Songs Uploaded";this.backend.lastPlaylist(a,function(a){a.length==0?djr.debug("No results found."):b.createChunk(a,c)})};djr.Player.prototype.randomPlaylist=function(a){var b=this,c=""+a+" Random Songs ";this.backend.randomPlaylist(a,function(a){a.length==0?djr.debug("No results found."):b.createChunk(a,c)})};
+djr.Player.prototype.mostPlayedPlaylist=function(a){var b=this,c=""+a+" Most Played Songs";this.backend.mostPlayedPlaylist(a,function(a){var e=[];$.each(a,function(a,b){e.push(b.sha1)});e.length==0?djr.debug("No results found."):b.createChunk(e,c)})};djr.Player.prototype.extendCurrentPlaylist=function(){var a=this;this.backend.moreLikeThese(this.playlist.allSongs(),function(b){a.createChunk(b,"suggestions")})};
 djr.Player.prototype.createChunk=function(a,b){var c=this.playlist.createUniqueChunk(a,b);if(c){this.playlist.chunks.length>1&&this.mergePlaylistChunks();var d=this.playlist.addChunk(c);this.savePlaylist();this.backend.getHtmlForSongs(a,function(a){this.hideAllChunks();this.setChunkHtml(c,d,a)},this)}else djr.debug("All the results are already in the playlist")};
 djr.Player.prototype.setChunkHtml=function(a,b,c){a=a.wrapHtml(b,c);$("#playlistDiv").append(a);var d=this,e=$("#chunk_"+b);e.find(".song_a").click(function(){d.play($(this).attr("id").substr(5))});e.find(".album_a").click(function(){d.search('(album:"'+$(this).text()+'")')});e.find(".chunk_title").click(function(){e.find(".chunk_inner").toggle()});e.hover(function(){$(this).find(".chunk_ctl_wrap .ctlbox").show()},function(){$(this).find(".chunk_ctl_wrap .ctlbox").hide()});e.find(".chunk_ctl_wrap .ctlbox .ctl_remove").click(function(){djr.debug("removing chunk "+
 b);d.removeChunk(b)});e.find(".chunk_inner .song").hover(function(){$(this).find(".ctlbox").show()},function(){$(this).find(".ctlbox").hide()});e.find(".chunk_inner .ctlbox .ctl_remove").click(function(){var a=$(this).parent().parent().attr("id").substr(5);d.removeSong(a)})};
diff --git a/server/djrandom/frontend/static/js/djr/Makefile b/server/djrandom/frontend/static/js/djr/Makefile
index 3075a035437c1834671fe1a5ab3dc4525c0dfb8e..7d483dbc1cde3db997329792888b3caf693ae2ba 100644
--- a/server/djrandom/frontend/static/js/djr/Makefile
+++ b/server/djrandom/frontend/static/js/djr/Makefile
@@ -1,5 +1,6 @@
 
-JSCOMPILER = java -jar /usr/bin/compiler.jar
+JSCOMPILER_JAR = /usr/lib/jscompiler/compiler.jar
+JSCOMPILER = java -jar $(JSCOMPILER_JAR)
 
 SOURCES = \
 	djr.js \
diff --git a/server/djrandom/frontend/static/js/djr/backend.js b/server/djrandom/frontend/static/js/djr/backend.js
index 2bf1fcb94a4964016764c7bde2bf0464334c48f7..1921dc102228286e3ec43b720160cb30fdcc1854 100644
--- a/server/djrandom/frontend/static/js/djr/backend.js
+++ b/server/djrandom/frontend/static/js/djr/backend.js
@@ -199,13 +199,14 @@ djr.Backend.prototype.lastPlaylist = function(num, callback ,ctx) {
 };
 
 /**
-  * Request N most played songs
+  * Return N pseudo-random songs based on the current playlist.
   *
   * @param {integer} n Number of songs requested
+  * @param {Array[string]} uuids SHA1 hashes of the current playlist
   *
   */
-djr.Backend.prototype.randomPlaylist = function(num, uuids, callback ,ctx) {
-  $.ajax({url: '/json/random',
+djr.Backend.prototype.markovPlaylist = function(num, uuids, callback ,ctx) {
+  $.ajax({url: '/json/markov',
           data: {'h': uuids.join(','),
                  'n': num },
           dataType: 'json',
@@ -217,3 +218,21 @@ djr.Backend.prototype.randomPlaylist = function(num, uuids, callback ,ctx) {
          });
 };
 
+/**
+  * Return N completely random songs.
+  *
+  * @param {integer} n Number of songs requested
+  *
+  */
+djr.Backend.prototype.randomPlaylist = function(num, callback ,ctx) {
+  $.ajax({url: '/json/random',
+          data: {'n': num },
+          dataType: 'json',
+          type: 'GET',
+          context: ctx || this,
+          success: function(data, status, jqxhr) {
+            callback(data.results);
+          }
+         });
+};
+
diff --git a/server/djrandom/frontend/static/js/djr/player.js b/server/djrandom/frontend/static/js/djr/player.js
index 6f9ff0b34646b7a1c5e0b9d3966d14941e5c861b..6900a126b4302ea823016d7841386a1a1613585c 100644
--- a/server/djrandom/frontend/static/js/djr/player.js
+++ b/server/djrandom/frontend/static/js/djr/player.js
@@ -118,8 +118,8 @@ djr.Player.prototype.lastPlaylist = function(num) {
 
 djr.Player.prototype.randomPlaylist = function(num) {
   var player = this;
-  var title = "Last " + num + " Random Songs ";
-  this.backend.randomPlaylist(num, this.playlist.allSongs(), function(results) {
+  var title = "" + num + " Random Songs ";
+  this.backend.randomPlaylist(num, function(results) {
     var songs = results;
     if (songs.length == 0) {
       djr.debug('No results found.');
@@ -133,7 +133,7 @@ djr.Player.prototype.randomPlaylist = function(num) {
 
 djr.Player.prototype.mostPlayedPlaylist = function(num) {
   var player = this;
-  var title = "Most " + num + " Played Songs";
+  var title = "" + num + " Most Played Songs";
   this.backend.mostPlayedPlaylist(num, function(results) {
     var songs = [];
     $.each(results, function(idx, item) {
diff --git a/server/djrandom/frontend/templates/_std_base.html b/server/djrandom/frontend/templates/_std_base.html
new file mode 100644
index 0000000000000000000000000000000000000000..eef79cf7d907e9963a66c3b4f25464903642e1a8
--- /dev/null
+++ b/server/djrandom/frontend/templates/_std_base.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+  <head>
+    <title>DJ RANDOM :: {% block title %}{% endblock %}</title>
+    <link href="/static/favicon.ico" type="image/x-icon" rel="shortcut icon">
+    <link rel="stylesheet" type="text/css" href="/static/css/login.css">
+  </head>
+  <body>
+
+    <div id="main">
+      <div id="form">
+      {% block main %}
+      {% endblock %}
+      </div>
+    </div>
+  </body>
+</html>
diff --git a/server/djrandom/frontend/templates/about.html b/server/djrandom/frontend/templates/about.html
index 5c737152e9022947036f2c600a6e57e729f73682..aedc42e9eca58f0f9c1b254169ec5298eedadc17 100644
--- a/server/djrandom/frontend/templates/about.html
+++ b/server/djrandom/frontend/templates/about.html
@@ -1,14 +1,7 @@
-<!doctype html>
-<html>
-  <head>
-    <title>DJ RANDOM :: About</title>
-    <link href="/static/favicon.ico" type="image/x-icon" rel="shortcut icon">
-    <link rel="stylesheet" type="text/css" href="/static/css/login.css">
-  </head>
-  <body>
+{% extends "_std_base.html" %}
+{% block title %}About{% endblock %}
+{% block main %}
 
-    <div id="main">
-      <div id="form">
         <h3>DJ:Random</h3>
 
         <p>
@@ -19,9 +12,4 @@
           (<b>{{ used_gb }}</b> Gb).
         </p>
 
-      </div>
-
-    </div>
-
-  </body>
-</html>
+{% endblock %}
diff --git a/server/djrandom/frontend/templates/redirect.html b/server/djrandom/frontend/templates/redirect.html
new file mode 100644
index 0000000000000000000000000000000000000000..a99041f8d0dbd8c4c6da4f7b034d247a94388af9
--- /dev/null
+++ b/server/djrandom/frontend/templates/redirect.html
@@ -0,0 +1,13 @@
+{% extends "_std_base.html" %}
+{% block title %}About{% endblock %}
+{% block main %}
+
+<p>
+  Redirecting to <a href="{{ url | e }}">{{ url | e }}</a> ...
+</p>
+
+<script type="text/javascript">
+  window.location = '{{ url | e }}';
+</script>
+
+{% endblock %}
diff --git a/server/djrandom/frontend/templates/user_details.html b/server/djrandom/frontend/templates/user_details.html
index 5afee9535746294fe59a30edbbc477f02703055a..6b0d9432d5a4541ee4d31b314003a8a6a0085040 100644
--- a/server/djrandom/frontend/templates/user_details.html
+++ b/server/djrandom/frontend/templates/user_details.html
@@ -1,13 +1,7 @@
-<!doctype html>
-<html>
-  <head>
-    <title>DJ RANDOM</title>
-    <link href="/static/favicon.ico" type="image/x-icon" rel="shortcut icon">
-    <link rel="stylesheet" type="text/css" href="/static/css/login.css">
-  </head>
-  <body>
-
-    <div id="main">
+{% extends "_std_base.html" %}
+{% block title %}User Details{% endblock %}
+{% block main %}
+
       <div id="form">
 
         {%- for msg in get_flashed_messages() -%}
@@ -55,5 +49,4 @@
 
     </div>
 
-  </body>
-</html>
+{% endblock %}
diff --git a/server/djrandom/frontend/templates/user_invite.html b/server/djrandom/frontend/templates/user_invite.html
index 2cc3afa87234d1a98592291b67108262b56a2a47..ee7625b4e39ef270fb1db98e02efb9abb7da611b 100644
--- a/server/djrandom/frontend/templates/user_invite.html
+++ b/server/djrandom/frontend/templates/user_invite.html
@@ -1,14 +1,7 @@
-<!doctype html>
-<html>
-  <head>
-    <title>DJ RANDOM</title>
-    <link href="/static/favicon.ico" type="image/x-icon" rel="shortcut icon">
-    <link rel="stylesheet" type="text/css" href="/static/css/login.css">
-  </head>
-  <body>
+{% extends "_std_base.html" %}
+{% block title %}Invites{% endblock %}
+{% block main %}
 
-    <div id="main">
-      <div id="form">
         <form action="/user/invite" method="post">
           {{ form.hidden_tag() | safe }}
 
@@ -27,9 +20,5 @@
             <input type="submit" class="f_submit" value=" Invite ">
           </p>
         </form>
-      </div>
 
-    </div>
-
-  </body>
-</html>
+{% endblock %}
diff --git a/server/djrandom/frontend/views.py b/server/djrandom/frontend/views.py
index 662b70170e82ddf89c282faf4c014075a084dbcc..ec792dd7e8b7defdc867fa880aefc95ea0d5c09c 100644
--- a/server/djrandom/frontend/views.py
+++ b/server/djrandom/frontend/views.py
@@ -52,6 +52,15 @@ def homepage():
     return render_template('index.html', user=user)
 
 
+@app.route('/ext')
+@require_auth
+def redirect_to_external_url():
+    url = request.args.get('url')
+    if not url:
+        abort(400)
+    return render_template('redirect.html', url=url)
+
+
 def fileiter(path, pos, end):
     with open(path, 'r') as fd:
         fd.seek(pos)
diff --git a/server/djrandom/model/mp3.py b/server/djrandom/model/mp3.py
index cb2c220243696195566e1879f2870a6a534d017d..f9314351cc7313dd1b1470c98a74443a85e450cf 100644
--- a/server/djrandom/model/mp3.py
+++ b/server/djrandom/model/mp3.py
@@ -1,3 +1,4 @@
+import random
 from sqlalchemy import *
 from datetime import datetime, timedelta
 from djrandom.database import Base, Session
@@ -42,6 +43,17 @@ class MP3(Base):
         return cls.query.filter_by(ready=True).order_by(
             desc(cls.uploaded_at)).limit(n)
 
+    @classmethod
+    def get_random_songs(cls, n=10):
+        """Return N completely random songs."""
+        results = []
+        num_songs = cls.query.filter_by(ready=True).count()
+        for idx in xrange(n):
+            song = cls.query.filter_by(ready=True).limit(1).offset(
+                random.randint(0, num_songs - 1)).one()
+            results.append(song)
+        return results
+
 
 class PlayLog(Base):