Self-hosted · Single Rust binary · v0.120.0

Not just what you played.
How you played it.

Tapedeck is a music intelligence hub for your own server. It records the full signal chain — source file, DAC, amp, transducer — alongside every listen, catalogues the vinyl and tape nothing can report automatically, and forwards to Last.fm and ListenBrainz while keeping the record yours.

740
tests, SQLite build
<5ms
per ingested listen
5
media servers polled
0
telemetry calls home
Deck idle fooyin · Fedora Sway
Teardrop
Massive Attack — Mezzanine
FLAC 24/96
1:14 / 5:30 Tape 0048
1:14
5:30
Signal chain resolved Desktop Reference rung 2 · output binding
Source
fooyin
Fedora Sway
DAC
Schiit Mimir
USB input
Amp
Schiit Midgaard
high gain
Transducer
Sennheiser HD 650
1,142 h
Codec FLAC 24/96 Quality 85/100 Context Active → Last.fm · ListenBrainz

A live mock of the dashboard deck. The reels turn only while something is actually playing — in the real app and here.

Most scrobblers store a title, an artist and a timestamp.

That is a list, not a history. It cannot tell you that your FLAC was degraded to SBC on the walk home, that eleven hundred hours have gone through the HD 650 drivers, or that the record you played last Sunday came off a shelf rather than a server. Tapedeck records all of it, next to the listen, from the moment it arrives.

Ingest
Everything already speaks it

A ListenBrainz-compatible API, so Pano Scrobbler, Web Scrobbler, multi-scrobbler and mpdscribble work with a URL and a token. Plex, Navidrome, Jellyfin, Emby and Roon are polled directly.

Proxy
You don't have to choose

Listens are stored locally and forwarded to Last.fm, Libre.fm and ListenBrainz. Per user, never server-wide. Each pending listen tracks which sinks accepted it, so a retry never double-scrobbles.

Yours
Nothing leaves that you didn't send

One binary, one SQLite file, credentials encrypted with a key held outside the database. One opt-in public endpoint, off until you switch it on.

Signal chain intelligence

You never have to tag a listen by hand.

Attribution happens at the source, but a chain lives downstream — so the server resolves it on a four-rung ladder, first match winning. The order encodes what each rung is: a binding is evidence about this listen, a token is a standing statement about one app, a device is the coarsest fallback.

Issue one token per app — fooyin, Pano on Android — give each a chain, and that is the whole setup. The app itself needs no Tapedeck-specific configuration.

Hours are derived from listens rather than accumulated, so fixing a chain today moves the wear for plays you already have. Skips bank nothing.

Click a rung to see which listen it would catch.

What the client sent
"tapedeck_device": { "output_device": "Schiit Mimir" }
→ resolves to Desktop Reference (learned output)
The Shelf

The listening nothing can report automatically.

Catalogue a pressing by barcode, catalogue number or name. Drop the needle, press Start, and the platter turns at a real 33⅓ with a countdown — the browser runs the clock, so a pause and a skip are recorded as they happen and every track lands where it actually played.

A side writes one listen per track, never one per side, because every counting query in Tapedeck is over listens. Stylus hours are derived from the sides played, so they cannot drift from the plays they claim to count.

VinylCassetteCDSACD
How the shelf works →
SAW
Side A · 12" vinyl · 33⅓
Selected Ambient Works 85–92
Aphex Twin
Yours for 4 years — bought used from Monorail, Glasgow
A1 Xtal 4:51
A2 Tha 9:01
A3 Pulsewidth 3:47
A4 Ageispolis 5:21
0:00
23:00

Playing this side writes four listens, one per track, each stamped as it actually started.

Lyrics theatre

Hands busy with a record. Words across the room.

A full-bleed view built for the listening you can't touch — a side is playing, the sleeve is out of reach, and there is nothing to tap. Timed lines follow the clock; untimed ones you move yourself.

5
0:19
5 · line 2 · 0:19 played 37 times
Haunted Room
The Sundowners · Hollow Tide
Timed
Side A
1 Long Way Down 3:35
2 Hollow Tide 3:57
3 Everybody's Fool Again 3:16
4 Farther Away 4:33
5 Haunted Room 3:07

Words are fetched per track and cached here only so a note can point at a line — they are not ours, and they never leave the instance: not in a backup, not in an export, not to anyone you have patched with. A liner note quotes a line by index, never by its words. The lines above are invented for this demo.

Statistics & musical personality

Every figure says what population it came from.

A lossless share is a fact about the listens whose format is known. Where that is too little of your history, the card reports its coverage instead of a number — because a share drawn from a twentieth of your listening is a fact about the twentieth, not a verdict on your library.

Listening clock
21,480 listens · your timezone
Mon
Tue
Wed
Thu
Fri
Sat
Sun
00
06
12
18
Quieter Busier Peak: Thursday, 23:00
Musical personality
The Obsessive Night Owl

Five explainable axes, computed locally. No external call, no comparison with anybody else.

AdventurerLoyalist
CuratorDrifter
CasualObsessive
LarkNight Owl
Mood-DrivenIdentity
Lossless share
64%
of the 3,140 listens whose format is known (15% of history)
Hours on the HD 650
1,142
derived from listens carrying Desktop Reference
Off the shelf
318
listens from 41 sides played, 27 records
This month ranks
2nd
busiest of your 8 — never a percentile against other people
Genre Map

Your listening, placed on Every Noise at Once.

Where your taste sits in the genre space, how it has moved over eight years, and which regions border what you already play. Hover a point.

organic ← → mechanical
atmospheric ← → spiky
IDM
2,208 listens · Aphex Twin, Autechre, Boards of Canada
This is a placeholder. Every Noise places artists, so today every track by one artist shares its sound coordinates. A plugin in development will compute per-recording features from the real files, through Plex, Jellyfin and OpenSubsonic.
Playlist Lab

Grow a playlist by moving through your own space.

Ten dimensions: six from the genre map reached through each track's artist, four from your own listening — era, familiarity, hour of day, recency. Pick the two you want to see.

x: organic ↔ mechanical
y: familiarity

Genres attach to artists, so every track by one artist shares its six sound coordinates — the listening axes are what separate them. Tracks with no place on the map are dropped and counted, never parked in the middle.

Liner Notes

What you thought at the time, kept next to the music.

A note is a living document about one thing — a track, a release, an artist, or a single playing — edited in place with every earlier version archived, so a rewrite years later never erases what you wrote in 2011.

Shortcodes resolve against things you've annotated and against your own listen history. A reference that matches nothing stays readable prose with a dotted underline — never an error, because most of a real library is un-enriched and those are exactly the records worth writing about.

[[track:3]][[artist:…]][[gear:…]][[line:12]]
Note on a release
Selected Ambient Works 85–92 Private
Rendered and sanitised server-side — shortcodes resolve in the same pass
Bought on a whim in 2011 and it rearranged the year. Pulsewidth is the one that got me — nothing else by Aphex Twin sounds quite like it, and Windowlicker came much later. Heard properly for the first time through Sennheiser HD 650. Grasshopper Rain was never real.
4 resolved 1 left as prose rev 4 · edited today
Patch

The other decks on your instance.

Everything else in Tapedeck is yours alone. Patch is the part that isn't: patch into another deck on your server and their listening appears on The Reel. A patch waits for a yes — being followed and being readable are separate states.

Pass a record along as a Dub and it lands in their Crate with your reason attached, never in their history. Or listen together in a Shared Spool, where plays wait in the other's inbox and enter no history until accepted.

Two switches, both off, and neither is per listen. Instance-wide figures stay chain-gated whichever you use, because those reach people you never approved.

Read about Patch →
Model Context Protocol

Ask a chatbot about your listening.

Twenty-six tools, all of them the same queries the web UI uses. Seven grants, every one off by default — your statistics and your journal are separate grants on purpose.

A playlist an assistant builds is saved into Tapedeck first and reaches a media server only when you send it. Every call is recorded — which tool, when, with what, how many rows came back.

And the rest of what it does.

Reports
Your listening in chapters

Week, month and year, each a genuinely different shape rather than one card resized. A month exports as a story, square or wide image — drawn on the server, so every client gets the same picture.

Rediscovery
Smart collections from your own history

Forgotten favourites, loved but barely played, one-listen records, the 3 AM club, dusty vinyl, and never heard on a particular pair of headphones. Send any of them to your media server.

Audio quality
A 0–100 score per listen

Codec, sample rate, bit depth, DSD rate and delivery format. Source versus delivered, so you know when your FLAC was degraded to AAC over Bluetooth. DSD is first-class.

Loved
Every heart in one place

Tracks, records and artists — a love attaches to an entity rather than only a song. Recording loves mirror out to Last.fm and ListenBrainz; the rest stay local because neither service has the concept. The star ratings in Plex, Navidrome, Jellyfin or Emby become loves above a threshold you set.

Import
Bring your history with you

The ListenBrainz export zip, a Spotify extended-history zip (re-anchored to when tracks started, not stopped), Last.fm by username, .json, .csv, or a Rockbox .scrobbler.log straight off a DAP.

Export
And take it away again

Full JSON or CSV, a consistent VACUUM INTO backup of the whole database, and liner notes as Markdown with working [[wikilinks]] for Obsidian or Logseq.

Sessions
Grouped, and editable

Contiguous listening under a 30-minute gap becomes a session with duration, track count, quality stats and context. Merge, split and rebuild them; the UUIDs stay stable.

Lyrics
For the language a title cannot give

Read from your own Plex or Jellyfin first, LRCLIB after, and cached outside your backup. A romanised Korean title falls through to English/Other; the lyric is where that title's script went. Instrumental is a real answer.

Skips
Kept, and never counted

Out of every listen count, chart and forward — a skip on your Last.fm is a wrong scrobble on a permanent record. They have their own readout showing what played either side.

Multi-user
A household on one instance

Own token, own history, own chains, own sources, isolated per user. Admins manage everyone from the Users screen, with guards against deleting yourself or the last admin.

Reliability
Built to be a system of record

WAL SQLite by default or Postgres in its own build, retryable 503s, per-sink delivery tracking, circuit breakers per service, graceful SIGTERM drain, and an Operations screen that tails the log.

Your own site
One opt-in public endpoint

GET /public/np/<user> returns what's on your deck right now. Four separate field switches, all off by default. A Hugo shortcode ships in the repo and degrades to nothing.

Plugins & clients

Anything that speaks the API works. Plugins go further.

A plugin exists where the wire format runs out — where a player knows the output device, the real signal path or the file that was actually decoded, and there is no field to put it in. Some are shipped, some are still being written, and the page says which.

Shipped

A native plugin for fooyin, reporting the output device, the decoded format and real skips the wire format has nowhere to put. Working — listens, skips and an offline queue exercised against a live deck, on Fedora 44 so far.

In development

Push from the Navidrome side rather than Tapedeck polling it from outside. Navidrome already works today as a polled source.

In development

Per-recording features computed from the real files via Plex, Jellyfin and OpenSubsonic — what replaces the Every Noise placeholder behind the Genre Map.

In development

The Android, Fire TV and iOS client — history and the shelf on the phone, barcode scanning onto it, the words for what is playing on the TV. Foundations only so far; desktop and Apple TV apps come after.

Known gaps

Things that look finished and aren't.

A page like this normally stops here. Tapedeck is a system of record, and a system of record that overstates itself is worse than one that admits a gap — so these are on the marketing page rather than three clicks into an issue tracker.

Emby is untested

One protocol with Jellyfin, driven from one client — but never run against a real Emby server. The fields at risk fail silently: no codec, no device, no MusicBrainz IDs, and nothing in the log to say so.

Roon is untested too

Built against the roon-api crate's declared types and Roon's vendored docs. Note that its extension API carries no signal path, no MusicBrainz IDs and no ratings — a Roon listen holds less detail than a Plex one.

Star ratings from Plex and Jellyfin are unverified

Turning ratings into loves works end to end against Navidrome. The Plex and Jellyfin/Emby walks are built from those servers' published API descriptions and pinned by tests, but have not been run against a real server of either kind.

Nothing federates outward yet

Actor endpoints, WebFinger and signature verification all serve. Delivery is the remaining work, and that order is deliberate: identity other servers cache has to be right before anyone holds a copy.

Audio quality is mostly unknown

Only listens a live source reported carry a format. On a real 21,000-listen history that is 97% unknown, which is why the fidelity cards report coverage rather than a share.

A side may be split by track count

When a lookup returns no running times, sides are split by number of tracks instead of playing time — which can put the break in the wrong place. Re-filing through the medium picker moves it.

The genre space is per-artist, for now

Every Noise at Once places artists, so every track by one artist shares its six sound coordinates — the Genre Map and Playlist Lab are a placeholder until per-recording features are computed from your real files.

Nothing reads the audio vectors yet

AudioMuse-AI can be connected, tested and measured, and that is the whole of it — the per-recording sound space it exists to supply is not wired into the Genre Map, Playlist Lab or anywhere else. The coverage measurement was built first on purpose: how much of a history can be joined at all is what decides whether the rest is worth building.

TIDAL's write half is unverified

Sign-in, search and token refresh are confirmed against the live API. Creating a playlist still uses the two POST shapes TIDAL's spec declares rather than ones observed working.

One binary. Your server. Your history.

No config files to hunt through and no token to copy out of a console — open the UI on first run and it walks you through creating the admin account.

Packaged for openSUSE, Fedora, Debian, Ubuntu and Arch; anywhere else, build it from source with Rust 1.88+ and Bun. A Docker image is in development. 512 MB of RAM and four ARM cores is the whole hardware requirement — not an estimate, but what demo.tapedeck.cc runs the real binary on: a passively-cooled Raspberry Pi Zero 2 W with a 64 GB SD card. On 64-bit Raspberry Pi OS the Debian arm64 package installs as it is, with nothing to compile on the Pi.

# after adding the repository for your distribution
$ sudo apt install tapedeck
$ sudo systemctl enable --now tapedeck
📋 Visit the web UI to complete setup.
→ http://your-server:8080