A single Rust daemon that speaks the MPD text protocol to clients (ncmpcpp, mpc, its own tools) and is itself an OpenSubsonic client + mpv audio player. It replaced a mopidy + mopidy-subidy Python stack in one process: MPD commands in, OpenSubsonic REST (browse / search / star / scrobble) and mpv-streamed audio out.
The name is from the Roman hypocaust (hypo- "beneath" + kaustos "burnt") - the furnace under the bath, tended out of sight. hypodj is the DJ underneath: it does the real work below while your client lounges in the warm room.
MPD client ──MPD text/TCP──▶ hypodj daemon ──OpenSubsonic REST──▶ Navidrome
(ncmpcpp, dj, dj-gui) │ (or any
▼ OpenSubsonic
libmpv audio server)
HypoDJ (dj-gui), the DJ View: the queue beside the natural-language DJ, with the now-playing card, up-next, and a live level meter.
Driving music should be human-native and natural-language-first: say what you want in plain words ("play something calmer", "fade out in 20 minutes") and the server handles the complexity. Natural language is translated server-side into a validated plan and echoed back before it arms - never a surprise. Playback is startle-safe: pause, resume, and skip fade instead of cutting, and volume moves like a physical fader, not a step function.
A ratatui terminal jukebox, pure MPD/TCP (no libmpv), event-driven via the daemon's idle-push socket with worker-thread IO so the UI never blocks on network.
- Now-playing card with dithered album-art cover, up-next preview, and a physical volume fader
- Four screens (
F1-F4): Queue / Albums / Playlists / DJ - the DJ screen is a natural-language Claude Code DJ pane beside the queue (shown above) - Vim-like navigation:
j/k,g/G, scrolloff;Shift+Pjumps the Queue cursor to the currently-playing song rstarts an endless radio from the row under the cursor - a song, an album, an artist, anywhere including the search hits - and the queue keeps refilling itself/incremental search withn/Nmatch cycling and matched-substring highlight:command line - verbs plus natural language with echo-before-arm confirm- Physical-potentiometer volume knob: perceptual dB detents, off-click pause
- Queue markers on albums (fully / partially enqueued) and songs
Pure MPD/TCP, no libmpv. Bare dj prints a now-playing card; dj next,
dj pause, dj vol 40 are quick verbs; anything else is natural language with
the same echo-before-arm confirm: dj "play something calmer". dj radio starts
the endless radio from whatever is playing (dj radio random from cold, dj radio off to stop).
Any stock MPD client (ncmpcpp, mpc) also works unchanged against the daemon.
| Area | What |
|---|---|
| MPD server | Hand-rolled TCP server: full ncmpcpp command surface, idle push, binary albumart/readpicture, sticker ratings, find/search, playlists |
| Library | OpenSubsonic browse/search3, smart album lists, genres, first-class favorites (songs / albums / artists), scrobbling, TTL+LRU listing cache |
| Playback | libmpv actor; startle-safe fades on pause/resume/skip; graduated + humanized absolute volume; sleep / wind-down / wake; smooth restart |
| Endless radio | radio starts a never-ending radio from a thing (radio, radio random, radio song/<id>, radio album/<id>, radio artist/<id>): it plays what you pointed at - bare radio picks the deck up where it stands (resumes a paused one, starts a stopped one, never restarts what is already playing) - prefetches one batch of similar library tracks behind it, and hands the end-of-queue edge to the continuation walk, which re-seeds from what just played and so keeps moving through your own library. radio on arms the walk without touching the queue; radio off disarms and restores the configured continuation mode |
| Intent | Deterministic capability core + typed Plan IR; the NL front-end (rules-first, optional local model) only ever emits a validated plan |
| Desktop | MPRIS, so GNOME media controls work |
nix run github:FamiliarTools/hypodj#dj # the CLI (default app)
nix run github:FamiliarTools/hypodj#dj-gui # HypoDJ, the TUI
The flake ships packages.hypodj (the daemon, libmpv wrapped),
packages.hypodj-clients (dj + dj-gui), and one shared services.hypodj
module for both NixOS and Home-Manager:
{
inputs.hypodj.url = "github:FamiliarTools/hypodj";
imports = [ hypodj.nixosModules.default ]; # or homeManagerModules.default
services.hypodj = {
enable = true;
server.url = "https://navidrome.example.com";
server.username = "you";
server.passwordFile = config.sops.secrets."hypodj/password".path;
};
}The password is read at service start into a 0600 runtime config - never the
Nix store (passwordCommand is the alternative to passwordFile). The service
runs headless (audio = "null") by default; set audio = "device" when hypodj
should own the speakers. Then point any client at mpd.bind
(e.g. ncmpcpp -h 127.0.0.1 -p 6601, the default; 6600 is reserved for mopidy,
so use it only if you set mpd.bind = "127.0.0.1:6600").
| Crate | What |
|---|---|
crates/hypodj-core |
The library: config, model, subsonic, player, MPD handler, plan/executor, fade, MPRIS |
crates/hypodj-daemon |
hypodj daemon binary + probe/play_probe live provers |
crates/hypodj-nl |
Natural language to validated Plan IR (rules + optional local model) |
crates/hypodj-client |
Shared client lib (MPD connection, nl handshake, routing) |
crates/hypodj-cli |
dj |
crates/hypodj-tui |
dj-gui (product name HypoDJ) |
Building, testing, and deploying are covered in CLAUDE.md.
