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.
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.
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.
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.
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.
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.
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.
How the shelf works →Playing this side writes four listens, one per track, each stamped as it actually started.
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.
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.
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.
Five explainable axes, computed locally. No external call, no comparison with anybody else.
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.
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.
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.
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.
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 →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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.