diff --git a/client/mpd/djmpd/djmpd.go b/client/mpd/djmpd/djmpd.go
new file mode 100644
index 0000000000000000000000000000000000000000..9308a6f32d4fde26d55d4f7108aed8eabf710449
--- /dev/null
+++ b/client/mpd/djmpd/djmpd.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+
+	"git.autistici.org/ale/gompd"
+	"git.autistici.org/ale/djrandom/client/mpd"
+)
+
+var (
+	port = flag.Int("port", 6600, "Port to listen on (MPD)")
+)
+
+func main() {
+	flag.Parse()
+
+	db := djmpd.NewDJRandomDatabase()
+	m := mpd.NewMpd(mpd.NewPortAudioPlayer(), db)
+	m.HandleURL("djrandom", db)
+	log.Fatal(m.ListenAndServe(fmt.Sprintf(":%d", *port)))
+}
diff --git a/client/mpd/djrandom.go b/client/mpd/djrandom.go
new file mode 100644
index 0000000000000000000000000000000000000000..b5ef82db2c7ded3d1077312e4f492404f2e099a7
--- /dev/null
+++ b/client/mpd/djrandom.go
@@ -0,0 +1,205 @@
+package djmpd
+
+import (
+	"bytes"
+	"errors"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net/url"
+	"strings"
+	"sync"
+
+	"git.autistici.org/ale/djrandom/api"
+	"git.autistici.org/ale/djrandom/client"
+	"git.autistici.org/ale/djrandom/util"
+	"git.autistici.org/ale/djrandom/util/config"
+
+	"git.autistici.org/ale/gompd"
+)
+
+var (
+	configFile = flag.String("djrandom-config", util.ExpandTilde("~/.djrandom.conf"), "Config file location")
+
+	serverUrl     = config.String("djrandom-server", "https://djrandom.incal.net", "Server URL")
+	authKeyId     = config.String("auth_key", "", "API authentication key")
+	authKeySecret = config.String("auth_secret", "", "API authentication secret")
+
+	djrandomOnce sync.Once
+)
+
+type DJRandomSong struct {
+	*api.Song
+
+	audiofile *api.AudioFile
+	client    *util.HttpClient
+}
+
+func newDJRandomSong(song *api.Song, client *util.HttpClient) *DJRandomSong {
+	af := song.GetBestAudioFile()
+	return &DJRandomSong{
+		Song:      song,
+		audiofile: af,
+		client:    client,
+	}
+}
+
+func (s *DJRandomSong) Channels() int {
+	if s.audiofile != nil {
+		return s.audiofile.Channels
+	}
+	return 2
+}
+
+func (s *DJRandomSong) SampleRate() float64 {
+	if s.audiofile != nil {
+		return float64(s.audiofile.SampleRate)
+	}
+	return 44100
+}
+
+func (s *DJRandomSong) URL() string {
+	return fmt.Sprintf("djrandom://%s", s.Id.String())
+}
+
+func (s *DJRandomSong) Info() string {
+	var buf bytes.Buffer
+	fmt.Fprintf(&buf, "file: %s\n", s.URL())
+	fmt.Fprintf(&buf, "Last-Modified: 2010-12-16T18:02:14Z\n")
+	if s.Meta.Artist != "" {
+		fmt.Fprintf(&buf, "Artist: %s\n", s.Meta.Artist)
+	}
+	if s.Meta.Title != "" {
+		fmt.Fprintf(&buf, "Title: %s\n", s.Meta.Title)
+	}
+	if s.Meta.Album != "" {
+		fmt.Fprintf(&buf, "Album: %s\n", s.Meta.Album)
+	}
+	if s.Meta.Genre != "" {
+		fmt.Fprintf(&buf, "Genre: %s\n", s.Meta.Genre)
+	}
+	if s.Meta.Year > 0 {
+		fmt.Fprintf(&buf, "Date: %d\n", s.Meta.Year)
+	}
+	if s.Meta.TrackNum > 0 {
+		fmt.Fprintf(&buf, "Track: %d\n", s.Meta.TrackNum)
+	}
+	if s.audiofile != nil {
+		fmt.Fprintf(&buf, "Time: %d\n", int(s.audiofile.Duration))
+	}
+	return buf.String()
+}
+
+func (s *DJRandomSong) Open() (io.ReadCloser, error) {
+	if s.audiofile == nil {
+		return nil, errors.New("no audio file")
+	}
+
+	resp, err := s.client.GetRaw("/dl/" + s.audiofile.MD5)
+	if err != nil {
+		return nil, err
+	}
+	if resp.StatusCode != 200 {
+		resp.Body.Close()
+		return nil, fmt.Errorf("HTTP error %d", resp.StatusCode)
+	}
+	return resp.Body, nil
+}
+
+type DJRandomDatabase struct {
+	client *util.HttpClient
+}
+
+func NewDJRandomDatabase() *DJRandomDatabase {
+	djrandomOnce.Do(func() {
+		if err := config.Parse(*configFile); err != nil {
+			log.Printf("Warning: could not read DJRandom config file: %v", err)
+		}
+	})
+
+	authKey := &api.AuthKey{
+		KeyId:  *authKeyId,
+		Secret: *authKeySecret,
+	}
+	client := util.NewHttpClient(*serverUrl, authKey, client.LoadCA())
+	return &DJRandomDatabase{
+		client: client,
+	}
+}
+
+func (d *DJRandomDatabase) doQuery(query string) ([]mpd.Song, error) {
+	log.Printf("literal query: %s", query)
+
+	values := url.Values{}
+	values.Add("q", query)
+	req, err := d.client.NewRequest("POST", "/api/search", strings.NewReader(values.Encode()))
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	var resp api.SearchResponse
+	if err := d.client.DoJSON(nil, req, &resp); err != nil {
+		return nil, err
+	}
+
+	// Convert []api.Song to []Song.
+	var out []mpd.Song
+	for _, s := range resp.Results {
+		out = append(out, newDJRandomSong(s, d.client))
+	}
+	return out, nil
+}
+
+func buildQuery(args []string, exact bool) string {
+	var qprefix string
+	if exact {
+		qprefix = "="
+	}
+	i := 0
+	var qparts []string
+	for i < len(args) {
+		i++
+		if i >= len(args) {
+			break
+		}
+		what := strings.ToLower(args[i-1])
+		q := args[i]
+		if what == "any" {
+			qparts = append(qparts, fmt.Sprintf("\"%s%s\"", qprefix, q))
+		} else {
+			qparts = append(qparts, fmt.Sprintf("%s:\"%s%s\"", what, qprefix, q))
+		}
+		i++
+	}
+	return strings.Join(qparts, " ")
+}
+
+func (d *DJRandomDatabase) Search(args []string) ([]mpd.Song, error) {
+	return d.doQuery(buildQuery(args, false))
+}
+
+func (d *DJRandomDatabase) Find(args []string) ([]mpd.Song, error) {
+	return d.doQuery(buildQuery(args, true))
+}
+
+func (d *DJRandomDatabase) GetSong(songURL *url.URL) (mpd.Song, error) {
+	songID := songURL.Host
+	if songID == "" {
+		return nil, errors.New("empty song ID")
+	}
+
+	req, err := d.client.NewRequest("GET", "/api/song/"+songID, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var resp api.Song
+	if err := d.client.DoJSON(nil, req, &resp); err != nil {
+		return nil, err
+	}
+
+	// Convert api.Song to Song.
+	return newDJRandomSong(&resp, d.client), nil
+}