diff --git a/server/djrandom/database.py b/server/djrandom/database.py
index b22413d037f569f3015e1b2c7ddf574a03ee0040..5b2fc49e8ee81c4d9ab34083f0e3419ed170428e 100644
--- a/server/djrandom/database.py
+++ b/server/djrandom/database.py
@@ -18,7 +18,7 @@ class SetTextFactory(PoolListener):
 
 
 def init_db(uri):
-    from djrandom.model import mp3
+    from djrandom.model import mp3, playlist
     engine = create_engine(uri, listeners=[SetTextFactory()])
     Session.configure(bind=engine)
     Base.metadata.create_all(engine)
diff --git a/server/djrandom/frontend/frontend.py b/server/djrandom/frontend/frontend.py
index 8a2a05ab18a9fd4b2fc7a12322ec5dd39e46bcab..e90b7fba5e640da980a625edf1529030f12152c9 100644
--- a/server/djrandom/frontend/frontend.py
+++ b/server/djrandom/frontend/frontend.py
@@ -2,7 +2,8 @@ import sys
 import os
 import optparse
 import logging
-from flask import Flask, request, Response, abort, jsonify, render_template
+from flask import Flask, request, Response, abort, jsonify, \
+    render_template, session
 from djrandom import daemonize
 from djrandom import utils
 from djrandom.model.mp3 import MP3
@@ -13,9 +14,38 @@ from sqlalchemy import distinct
 
 log = logging.getLogger(__name__)
 app = Flask(__name__)
+app.secret_key = 'J@9als[13- "!>0@!!zWz}='
 storage_root = None
 searcher = None
 
+USERID_COOKIE = 'USERID'
+
+
+@app.before_request
+def check_session_before_request():
+    # Recover the user id from the long-term cookie, if present,
+    # otherwise just generate a new one.
+    if 'userid' not in session:
+        if USERID_COOKIE in request.cookies:
+            session['userid'] = request.cookies[USERID_COOKIE]
+        else:
+            session['userid'] = utils.random_token()
+            session['_please_save_userid'] = True
+
+
+@app.after_request
+def save_session_after_request(resp):
+    if session.get('_please_save_userid'):
+        del session['_please_save_userid']
+        resp.set_cookie(USERID_COOKIE, value=session['userid'],
+                        expires=(2<<30) - 1)
+    return resp
+
+
+@app.teardown_request
+def shutdown_dbsession(exception=None):
+    Session.remove()
+
 
 @app.route('/json/artists')
 def all_artists_json():
@@ -59,6 +89,30 @@ def play_callback(sha1):
     return jsonify(status=True)
 
 
+@app.route('/json/playlist/save', methods=['POST'])
+def save_playlist():
+    hashes = request.form.get('h', '')
+    uuid = request.form.get('uuid')
+    if uuid:
+        playlist = Playlist.query.get(uuid)
+        if not playlist:
+            abort(404)
+    else:
+        playlist = Playlist(uuid=uuid, userid=session['userid'])
+    playlist.modified_at = datetime.now()
+    Session.add(playlist)
+    Session.commit()
+    return jsonify(uuid=uuid, status=True)
+
+
+@app.route('/json/playlist/get/<uuid>')
+def playlist_info_json(uuid):
+    playlist = Playlist.query.get(uuid)
+    if not playlist:
+        abort(404)
+    return jsonify(playlist.to_dict())
+
+
 @app.route('/json/search')
 def search_json():
     query = request.args.get('q')
@@ -92,7 +146,7 @@ def songs_fragment():
 
 @app.route('/')
 def homepage():
-    return render_template('index.html')
+    return render_template('index.html', userid=session['userid'])
 
 
 def fileiter(path, pos, end):
diff --git a/server/djrandom/frontend/static/djrandom.js b/server/djrandom/frontend/static/djrandom.js
index 63a546403071a90411f6643d92437c79b86d07ec..92f5f076ae154440b86f19a4e683fa6697643f98 100644
--- a/server/djrandom/frontend/static/djrandom.js
+++ b/server/djrandom/frontend/static/djrandom.js
@@ -3,7 +3,9 @@
 djr = {};
 
 // Global state.
-djr.state = {};
+djr.state = {
+  userid: null
+};
 
 // Debugging.
 djr.debug = function(msg) {
@@ -92,8 +94,10 @@ djr.doSearch = function() {
                   hashes.push(item.sha1);
                 }
               });
+              // Clear the current playlist and save a new one.
+              djr.state.player.clearPlaylist();
               djr.state.player.setPlaylist(hashes);
-              djr.debug('new playlist: ' + hashes.join(', '));
+              djr.state.player.savePlaylist();
 
               // Load the HTML rendering of the playlist.
               djr.loadPlaylistHtml();
@@ -102,7 +106,9 @@ djr.doSearch = function() {
 
 
 // Initialization.
-djr.init = function() {
+djr.init = function(userid) {
+  djr.state.userid = userid;
+
   // Restore state if the URL changes.
   $(window).bind('hashchange', djr.history.restore);
 
diff --git a/server/djrandom/frontend/static/player.js b/server/djrandom/frontend/static/player.js
index 0b367fe740e3f75d15cbc5b119c98e831fee5d21..923014f33e0029b791fd9267550f21744fd3dd08 100644
--- a/server/djrandom/frontend/static/player.js
+++ b/server/djrandom/frontend/static/player.js
@@ -3,6 +3,7 @@
 djr.Player = function(selector) {
   this.player = $(selector);
   this.curPlaylist = Array();
+  this.curPlaylistId = null;
   this.curIdx = -1;
   this.curSong = null;
 
@@ -28,15 +29,45 @@ djr.Player.prototype.removeFromPlaylist = function(sha1) {
 // Empty the current playlist.
 djr.Player.prototype.clearPlaylist = function() {
   this.curPlaylist = Array();
+  this.curPlaylistId = null;
   this.curIdx = -1;
 };
 
 // Set the playlist to a new set of songs.
 djr.Player.prototype.setPlaylist = function(hashes) {
+  djr.debug('new playlist: ' + hashes.join(', '));
   this.curPlaylist = hashes;
   this.curIdx = -1;
 };
 
+// Save the current playlist.
+djr.Player.prototype.savePlaylist = function() {
+  $.ajax({url: '/json/playlist/save',
+          data: {uuid: this.curPlaylistId,
+                 h: this.curPlaylist.join(',')},
+          dataType: 'json',
+          type: 'POST',
+          context: this,
+          success: function(data, status, jqxhr) {
+            if (data.status && data.uuid != this.curPlaylistId) {
+              this.curPlaylistId = data.uuid;
+              djr.debug('created new playlist, UUID=' + data.uuid);
+            }
+          }
+         });
+};
+
+// Load a playlist.
+djr.Player.prototype.loadPlaylist = function(uuid) {
+  $.ajax({url: '/json/playlist/get/' + uuid,
+          dataType: 'json',
+          context: this,
+          success: function(data, status, jqxhr) {
+            this.setPlaylist(data.songs);
+          }
+         });
+};
+
 // Start playing the next song in the playlist.
 djr.Player.prototype.nextSong = function() {
   var next_idx = this.curIdx + 1;
diff --git a/server/djrandom/frontend/templates/index.html b/server/djrandom/frontend/templates/index.html
index 0f339cbc060644313c2b94c8b3f5757dcc14ad7e..32cf04315eeddc1e0932811667ca82ab78ccb1e1 100644
--- a/server/djrandom/frontend/templates/index.html
+++ b/server/djrandom/frontend/templates/index.html
@@ -29,7 +29,9 @@ $(document).ready(function() {
     });
   $('#queryField').focus();
   $('#searchForm').submit(do_search);
-  djr.init();
+
+  // Initialize DJR with the current userid.
+  djr.init('{{ userid }}');
 });
 </script>
 {% endblock %}
diff --git a/server/djrandom/model/mp3.py b/server/djrandom/model/mp3.py
index 4d7337b34e137d696e9b98210a1987d2ec866601..4a69385e7068873cdeae19f7be5e1130134c24c0 100644
--- a/server/djrandom/model/mp3.py
+++ b/server/djrandom/model/mp3.py
@@ -21,7 +21,7 @@ class MP3(Base):
     album = Column(Unicode(256))
     genre = Column(Unicode(64))
     uploaded_at = Column(DateTime())
-    play_count = Column(Integer(default=0))
+    play_count = Column(Integer(), default=0)
 
     IDX_SHA1 = 0
     IDX_PATH = 1
diff --git a/server/djrandom/model/playlist.py b/server/djrandom/model/playlist.py
new file mode 100644
index 0000000000000000000000000000000000000000..e60c568542474ad1488454d0198d159408713c0d
--- /dev/null
+++ b/server/djrandom/model/playlist.py
@@ -0,0 +1,26 @@
+import os
+import hashlib
+from sqlalchemy import *
+from djrandom import utils
+from djrandom.database import Base
+
+
+class Playlist(Base):
+
+    __tablename__ = 'playlists'
+
+    uuid = Column(String(40), primary_key=True)
+    userid = Column(String(40), index=True)
+    modified_at = Column(DateTime())
+    play_count = Column(Integer(), default=0)
+    contents = Column(Text())
+
+    def __init__(self, **kw):
+        if kw and 'uuid' not in kw:
+            kw['uuid'] = utils.random_token()
+        for k, v in kw.items():
+            setattr(self, k, v)
+
+    def to_dict(self):
+        return {'uuid': self.uuid,
+                'songs': self.contents.split(',')}
diff --git a/server/djrandom/utils.py b/server/djrandom/utils.py
index 3b350e3823236c9e142d3bfd31bbef8228dce7c8..0f8c8593342a2b08c24a3d54906a326b944c6eb8 100644
--- a/server/djrandom/utils.py
+++ b/server/djrandom/utils.py
@@ -1,18 +1,22 @@
 import hashlib
 import os
 
-NESTING = 2
+PATH_NESTING = 2
 
 
 def generate_path(base_dir, sha1):
     dir_parts = [base_dir]
-    dir_parts.extend(sha1[:NESTING])
+    dir_parts.extend(sha1[:PATH_NESTING])
     base_path = os.path.join(*dir_parts)
     if not os.path.isdir(base_path):
         os.makedirs(base_path)
     return os.path.join(base_path, sha1)
 
 
+def random_token():
+    return hashlib.sha1(os.urandom(20)).hexdigest()
+
+
 def sha1_of_file(path):
     with open(path, 'r') as fd:
         sha = hashlib.sha1()