Docs

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.

$ git clone https://codeberg.org/abksh/tapedeck.git
$ cd tapedeck
$ cargo build --release
$ ./tapedeck
📋 Visit the web UI to complete setup.

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

Rust 1.88+ to build — time, zip and home in the lockfile set the floor
Bun, to build the embedded web UI. The packages need neither
A Plex, Navidrome, Jellyfin, Emby or Roon server — optional; the ingest API works standalone
A ListenBrainz / Last.fm / Libre.fm account — optional, connected per user
An AudioMuse-AI instance — optional, and on its own machine: it wants 4 cores and 8 GB of RAM, which is far more than Tapedeck
It runs on very little

512 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

1 Settings → Scrobble services
2 Add a custom ListenBrainz server
3 URL: http://your-server:8080
4 Token: your td_ token

This one client covers a Walkman, an Android phone and desktop Linux via MPRIS.

Settings └─ Scrobble services └─ Add custom ListenBrainz URL: http://your-server:8080 Token: td_… ✓ validate-token returns your username

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.

{ "listen_type": "single", "payload": [{ "listened_at": 1712000000, "track_metadata": { "artist_name": "Simon & Garfunkel", "track_name": "The Sound of Silence", "release_name": "Wednesday Morning, 3 A.M.", "additional_info": { "submission_client": "tapedeck-test", "duration_ms": 210000, "recording_mbid": "8f3471b5-7e6a-4d3c-9d28-1f0c0f0b0a11", "release_group_mbid": "b2f1e6c0-3a44-4f5e-8c6d-2a9b7e5c1d33", "work_mbid": "c41d0e2a-55b7-4a19-bf3e-6d8c2f7a4e90", "tapedeck_audio": { "format_type": "pcm", "codec": "FLAC", "sample_rate": 44100, "bit_depth": 16, "channels": 2, "is_lossless": true }, "tapedeck_device": { "player_name": "fooyin", "platform": "linux", "machine_id": "desktop-001", "output_device": "Schiit Mimir" }, "tapedeck_chain": { "chain_id": "desktop-reference" } } } }] }
tapedeck_audio

Codec, 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_device

Player name, platform, machine id and — the useful one — output_device. Every distinct output is auto-learned and mapped to a chain once.

MusicBrainz ids

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_chain

Names 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 holds
read 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 profile
write Loves, notes, editing or deleting a listen, and the shelf: barcode lookups, adding and editing a record, its scans, running times, playing a side
all read + write

A 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.

GrantWhat it allows
listening:readHistory, statistics, reports, sessions, skips, the genre map
library:readGear, signal chains, the shelf, loves, the Crate, liner notes
playlists:writeSave playlists into Tapedeck for you to review
playlists:pushSend them on to your media server
loves:writeLove and unlove
notes:writeWrite liner notes, recorded as written by that connection
crate:writeSuggest 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.

PORT=8080 HOST=0.0.0.0 SQLITE_DB_PATH=./tapedeck.db RUST_LOG=info MUSICBRAINZ_CONTACT=you@example.com

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.

GET /public/np/<username> {{< tapedeck-now-playing src="https://tapedeck.example.com/public/np/you" >}}

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.