From a clone to a deck with your history on it.
Almost nothing is configured in files. Scrobble connections and media sources are per user, set in the UI, and applied within a poll interval without a restart.
Build from source
On openSUSE, Fedora, Debian, Ubuntu or Arch, install the package instead: it needs no toolchain, and it comes with a systemd service. Anywhere else, cargo build is the whole build: build.rs compiles the SvelteKit UI
and rust-embed bakes it into the binary, so a fresh clone compiles.
Open http://your-server:8080 and it redirects to /setup to name the
admin account and set a password. Nothing to copy out of a console.
Bun is required for every cargo invocation, including check and clippy. For Rust-only work against an existing static/, set TAPEDECK_SKIP_WEB_BUILD=1. A Docker image is in development.
Listening is stored in SQLite by default. cargo build --release --features postgres builds against Postgres instead, with the caches in the same database, so DATABASE_URL is the only URL to set. The backend is fixed at compile time, and
there is no migration tool yet: a Postgres build opens a fresh database, not your SQLite
one. The packages are the SQLite build.
Requirements
time, zip and home in the
lockfile set the floor512 MB of RAM and four ARM cores. The public demo at demo.tapedeck.cc is a Raspberry Pi Zero 2 W with a 64 GB SD card, passively cooled — one binary, one SQLite file, no fan. That is the running requirement; building on it is another matter. On 64-bit Raspberry Pi OS, the Debian arm64 package skips the build. Anywhere else, compile on a bigger machine and copy the binary across.
Connect a client
Mint a token in Settings → API Tokens, give it the chain that app plays through, and point the client at your URL. That is the whole setup — the app itself needs no Tapedeck-specific configuration.
Pano Scrobbler
This one client covers a Walkman, an Android phone and desktop Linux via MPRIS.
Submitting a listen with everything attached
Three extended objects live in additional_info beyond the ListenBrainz spec and
round-trip intact — alongside the spec's own MusicBrainz ids, which are stored in full rather
than the two or three most clients bother with. This is the shape a plugin writes.
tapedeck_audioCodec, sample rate, bit depth, channels, DSD rate, lossless flag and delivery format. Feeds the 0–100 quality score and the source-versus-delivered comparison.
tapedeck_devicePlayer name, platform, machine id and — the useful one — output_device. Every
distinct output is auto-learned and mapped to a chain once.
Plain ListenBrainz fields, but the tagged-file ones no media server exposes: release_group_mbid groups the reissues, work_mbid ties every
performance of one composition together. Send them if your files carry them.
tapedeck_chainNames a chain outright when the client knows it. Rung one of the ladder, and the only one that is evidence about this particular listen.
Auth, in two credentials that are not interchangeable
A session — the td_session cookie from the web UI — can do
everything. An API token carries explicit scopes, and none of them implies
another. Matching is exact, so a scope of rewrite does not grant write.
submit POST /1/submit-listens, and GET /api/v1/scrobble-settings for the rule a listen is judged by. Nothing else — what every scrobble client holdsread History, stats, charts, search, reports, sessions, Rediscovery, album and artist pages, loves, notes, lyrics, now-playing, chains, gear, the shelf, saved playlists, the Crate, the genre map, your own profilewrite Loves, notes, editing or deleting a listen, and the shelf: barcode lookups, adding and editing a record, its scans, running times, playing a sideall read + writeA valid token missing the needed scope gets 403, not 401 — it authenticated fine, and a 401 would send a client into a refresh loop it cannot win.
Permanently session-only
Whatever scopes a token carries: everything under /admin/, plus sources,
service connections, Discogs settings, export and backup, the metadata sanitiser, the
enrichment jobs, and clearing every listen. Removing a shelf record or one of its scans
too: uploaded scans are not in the backup, so a deleted one is gone.
The spec is served by the binary
GET /api/openapi.yaml, deliberately unauthenticated — it documents shapes, not
data, and a client has to read it before it has a credential. It is generated with utoipa
from the handlers' own types, and tapedeck openapi prints it without starting
a server. A drift test walks every route in both directions, so the build fails rather than
sending you to an endpoint that 404s.
The same document is rendered here as the API reference — every endpoint and the auth it takes. Ask your own deck rather than this page when the two
could differ: it is the only thing that knows what version it is running.
Credentials at rest
Anything Tapedeck has to replay is encrypted with a key held outside the database — tapedeck.key or TAPEDECK_SECRET_KEY. Back it up separately, never
inside a database snapshot, which would defeat the point.
Connect an AI assistant
Tapedeck speaks the Model Context Protocol. For a local client, mint a credential in Settings →
AI Connections and point it at https://your-tapedeck/mcp. For a hosted one, give it
the same address and approve the OAuth request when it appears.
| Grant | What it allows |
|---|---|
listening:read | History, statistics, reports, sessions, skips, the genre map |
library:read | Gear, signal chains, the shelf, loves, the Crate, liner notes |
playlists:write | Save playlists into Tapedeck for you to review |
playlists:push | Send them on to your media server |
loves:write | Love and unlove |
notes:write | Write liner notes, recorded as written by that connection |
crate:write | Suggest a record you don't own — into the Crate and nowhere else |
All seven are off by default. Twenty-six tools, all of them the same queries the web UI uses.
The one to know about is listening_profile: it reports how much of your history
carries each kind of metadata and the assistant is told to call it first — which is what stops
"4% lossless" being said about a history where the format is simply unknown for 97% of listens.
Tested against a hosted Claude subscription over OAuth and Mistral Chat on the free tier — one
after the initialize handshake older revisions use, one on the stateless shape. Any
other MCP client should work; none has been tried.
What's left in files
Only infrastructure that has to exist before the UI can be served. Copy .env.example to .env, or tapedeck.toml.example to tapedeck.toml — precedence is env > .env > TOML.
Optional service credentials — Discogs, Apple Music, an AudioMuse-AI sidecar, a metrics token — are all optional and unconfigured is a normal state: the provider steps aside rather than erroring. The sidecar can also be set in the UI, under Settings → Connections, which is where it belongs: it is one connection for the whole server rather than a per-user one.
Your listening on your own site
One endpoint answers without a credential, and it is off until you switch it on. Title, artist and record are the whole of it by default; cover, position, format and chain are four separate switches.
It never reads your history — the deck is one track held in memory. A username that doesn't exist and one that hasn't published return the same 404, so nobody can use it to find out who has an account on your instance.
Try it before you install it
The demo instance runs the real binary on a passively-cooled Pi Zero 2 W, with eight years of generated listening on it.