-
-
Notifications
You must be signed in to change notification settings - Fork 45
Build a Streaming Box
This is the guided version of the Headless Daemon manual: one worked example, from a bare board to a silent always-on streamer wired to your DAC that you drive from the Qobuz app on your phone, from any terminal on your LAN, or from your own scripts.
The example rig is a Raspberry Pi 4 with a HiFiBerry DAC+ HAT — but the Pi is the example, not the target. Everything below works identically on any always-on Linux box: an old laptop in a closet, a NUC behind the TV, an LXC container, a fanless mini PC. Wherever a step is Pi-specific it says so; everywhere else, substitute your own hostname and DAC.
Time budget: about 30 minutes, all of it over SSH. No screen, no keyboard, no desktop environment on the box.
| Piece | Example | Substitute with |
|---|---|---|
| Box | Raspberry Pi 4B (4 GB), Raspberry Pi OS Bookworm | any Linux box/container that's always on |
| DAC | HiFiBerry DAC+ (I2S HAT) | any USB DAC — often the simpler option |
| Network | Ethernet | Wi-Fi works; wired is happier for hi-res |
| Access | SSH from your desktop | that's it |
Released qbzd binaries are built against glibc 2.35, so they run unmodified on Raspberry Pi OS / Debian Bookworm and similarly-aged distros — no toolchain, no compiling on the Pi.
HiFiBerry-style HAT only: enable the overlay and disable onboard audio in /boot/firmware/config.txt (dtoverlay=hifiberry-dacplus, dtparam=audio=off), then reboot — see HiFiBerry's own docs for your exact board. A USB DAC needs none of this; plug it in and it enumerates.
Grab the aarch64 tarball (for a Pi; x86_64 for a PC) from the latest release, copy it over, and install:
scp qbzd-*.tar.gz pi@streambox:
ssh pi@streambox
tar xf qbzd-*.tar.gz && cd qbzd-*
sudo install -Dm755 qbzd /usr/bin/qbzd
sudo install -Dm644 qbzd.service /usr/lib/systemd/user/qbzd.service
systemctl --user daemon-reload
qbzd versionNot on systemd? qbzd service prints a ready-to-install OpenRC/runit/systemd file with the install steps as comments — see Install on the manual page.
Two ways. Pick one.
Fresh login (nothing configured anywhere yet):
qbzd loginLogin is browser-based OAuth — the printed URL automatically uses the box's LAN address, so paste it into the browser on your phone or desktop, approve, done. No password ever touches the terminal. (Stuck? qbzd login --paste and the other fallbacks are on the manual page.)
Bring your desktop settings instead — if you already have quality preferences, gapless, and a tuned setup in desktop QBZ, hand them off rather than re-doing them in a TUI:
# on the desktop box
qbzd settings export --from desktop --include-auth
scp qbz-settings-*.qbzb pi@streambox:
# on the box
qbzd settings import qbz-settings-*.qbzb --include-auth
shred -u qbz-settings-*.qbzb # then delete the desktop copy too--include-auth must be passed on both sides or credentials are deliberately skipped. The bundle file is your logged-in session — treat it like a password: move it over SSH only, delete it at both ends once imported.
The one gotcha: the bundle carries the source machine's audio config and Connect name — your desktop's PipeWire device and its
QBZ (desktop)identity. Always redo Step 3 and Step 5's naming after an import, or the box will collide with your desktop's identity in the Qobuz app.
First, ask ALSA what your DAC is called:
aplay -L | grep -A1 ^hw:On the example Pi the HiFiBerry shows up as hw:CARD=sndrpihifiberry,DEV=0. Use that CARD= name form, not hw:0,0 — card numbers can reshuffle between boots; card names don't.
Now configure — either interactively:
qbzd setup # → Audio → Backend: ALSA → Output device: pick the DAC
…or in three non-interactive lines (handy for scripting a box build):
qbzd settings set audio.backend alsa
qbzd settings set audio.device "hw:CARD=sndrpihifiberry,DEV=0"
qbzd settings set audio.alsa_plugin hwhw means direct hardware access: no mixer, no resampler, nothing between the decoder and the DAC. That's the bit-perfect path — a 96 kHz/24-bit track leaves the box at 96 kHz/24-bit, and if your DAC has a display you'll see the rate switch per track. If PipeWire happens to be installed (Raspberry Pi OS ships it), it doesn't matter: with the ALSA backend the daemon opens the device directly and PipeWire is simply bypassed.
systemctl --user enable --now qbzd
sudo loginctl enable-linger $USERLinger is the step people skip and then wonder why the box vanished. Without it, your user services die the moment your SSH session closes. qbzd status warns you if it's off.
Then the one command to confirm everything at once:
qbzd statusOne line each for auth, audio device, playback, Qobuz Connect, and network. Anything wrong names itself here.
Give the box its public name first (this is what shows in the Qobuz app):
qbzd qconnect name "Living Room"From your phone — open the Qobuz app, open the Connect device picker, choose Living Room. Playback starts on the box's DAC; the phone is just the remote. This is the everyday mode: nothing to install, everyone in the house already knows how to use it.
From any terminal on your LAN — the CLI is a full remote control, not just play/pause. Point it at the box once:
export QBZD_HOST=streambox:8182 # or an IP; put it in your shell rcThen the basic loop is search → play:
qbzd search "dark side of the moon" --type album
qbzd play album:<ID> # an id from the search results…
qbzd play https://open.qobuz.com/album/… # …or just paste a share URL
qbzd now
qbzd volume 35qbzd play swallows anything — track:/album:/artist:/playlist: ids, Qobuz share URLs, even Deezer links (resolved by metadata match). Bare qbzd play resumes.
Beyond that, the whole catalog is there:
qbzd radio artist:<ID> # endless radio from a seed artist/track/album
qbzd discover new-releases # the Discover rails, in your terminal
qbzd artist <ID> --top # an artist's top tracks, with ids
qbzd queue list # what's coming up
qbzd queue add <TRACK_ID> --next # cut the lineEvery list verb takes --ids to print bare ids for piping. Two verbs read ids from stdin — playlist add <ID> - and suggest --seed - — so you can chain them:
# dump an album's tracks into one of your playlists
qbzd album <ID> --ids | qbzd playlist add <PLAYLIST_ID> -
# queue up suggestions seeded from what's playing right now
qbzd suggest --ids | head -5 | while read id; do qbzd queue add "$id"; doneThe full verb tables are in the CLI Cheat Sheet.
This is where a headless box earns its keep. Three building blocks: the CLI (scriptable, frozen exit codes), qbzd watch (push events, no polling), and the plain HTTP API on :8182.
Wake up to music — a cron job on the box:
30 6 * * 1-5 /usr/bin/qbzd volume 20 && /usr/bin/qbzd play playlist:<ID>
0 8 * * 1-5 /usr/bin/qbzd stop(On the box itself no QBZD_HOST is needed; from another machine, set it in the crontab line.)
React to what's playing — qbzd watch streams daemon events as newline-delimited JSON. A desktop notification for every track change, from your desktop, pointed at the box:
QBZD_HOST=streambox:8182 qbzd watch \
| jq --unbuffered -r 'select(.type=="TrackStarted") | .data.track.title' \
| while read -r title; do notify-send "Living Room" "$title"; doneThe same stream feeds a status-bar module, an LED panel, an OBS overlay — anything that would otherwise poll.
Love the current track from anywhere:
qbzd fav add track --currentBind that to a hotkey on your desktop and you can favorite what's playing in the living room without leaving your editor.
No CLI at all — plain HTTP. Everything above is curl-able, which is all a home-automation system needs:
curl -X POST http://streambox:8182/api/playback/toggle
curl http://streambox:8182/api/now-playingAs a Home Assistant rest_command:
rest_command:
living_room_toggle:
url: "http://streambox:8182/api/playback/toggle"
method: POSTGET /api/now-playing returns {"playback": …, "track": …} (track is null when idle) for a REST sensor, and GET /api/artwork/current is a redirect to the live cover art — point an <img> straight at it. On a Pi, the classic move is a GPIO button whose handler is exactly that one curl line: a physical play/pause button on the shelf.
Health check — exit codes are frozen across every verb, so monitoring needs no parsing:
qbzd ping || echo "streambox is down" | mail -s "qbz alert" you@example.com3 = daemon unreachable, 4 = needs re-login, 5 = audio device trouble — the full table is on the manual page.
The control port is deliberately LAN-first, like Sonos or MPD: anyone on your network can drive playback, nobody can read your credentials (they never cross the API). An always-on origin shield blocks browser-based abuse, and you can opt into a bearer token or bind to 127.0.0.1 if your LAN isn't fully trusted. It is plain HTTP — never port-forward it to the internet. Details: Network & Security Posture.
-
Silent, or anything weird →
qbzd status, always first. It names the failing subsystem and the fix. - Box disappears from the Qobuz app after you log out → linger is off (Step 4).
-
DAC not in the device list → re-plug, then
r(refresh) on the Audio screen ofqbzd setup; checkaplay -Lsees it at all. -
Two boxes fighting over one identity → you imported a bundle and skipped the naming gotcha in Step 2 — give each node its own
qbzd qconnect name.
More on the manual page's Troubleshooting section.
See also: Headless Daemon · Audio Configuration · HiFi Wizard · Casting and Remote Control
Getting Started
Audio
Appearance
My QBZ
Features
Help