The recollection that this used to exist is false — `git log --all -S
mpris` finds nothing on any branch. What has always been there is the
desktop *notification* on a track change, which is D-Bus but not MPRIS: a
popup is neither a status-bar entry nor a key target. So this is new, not
a regression.
Everything the protocol needs was already on the wire, so cbd-tui gains a
second front-end onto the two channels it already has: stream updates in,
MessageFromUi out. The MPRIS player is a peer of the UI thread — it
commands the server through the very channel the keybindings use, and it
learns the result the way the UI learns about a keypress from another
client. No proto change, no server change.
The decisions worth knowing (architecture/mpris.md):
- An absolute protocol over a toggling server. Play/Pause/SetShuffle/
SetLoopStatus consult the last state the server broadcast and send
nothing when it already matches, or the pause key would start playback
on a paused player. Volume is the same idea with arithmetic; muting is
spelled "volume 0", and the setter mutes on a zero target so the level
survives to be unmuted to.
- No URL reaches the bus. xesam:url would have to be the stream URL,
which clients never see and which several providers sign with
credentials, and every peer on a session bus can read properties. The
trackid is the queue position — also the only spelling that is a valid
object path.
- mpris:length is omitted when unknown rather than sent as zero, which
would make consumers draw a full progress bar.
- Unrepresentable requests are refused, not approximated: repeat-one,
rates other than 1.0, OpenUri, Raise, and Quit — a status-bar button
has no business closing someone's terminal.
- No session bus is a normal way to run (ssh, a tty, a container): the
connection carries a timeout and its failure is an info log, after
which the client behaves exactly as before.
Behind the `mpris` feature, on by default beside `notifications` and
forwarded by `cbd`; the nix package names it in headlessFeatures, since
naming a feature set at all replaces the crate defaults. It costs one
crate and no system library — zbus speaks D-Bus in pure Rust and
notify-rust had already brought it in.
Verified with the real thing, not only a test double: under
dbus-run-session, playerctl lists the player, reads its metadata
("Playing: the artist - the song (4:00)"), and drives play-pause,
`position 30+` and `volume 0.8` into the right commands. It also refuses
`next` when the queue is empty, which is CanGoNext being honest. The
committed bus test covers the round trip and skips where there is no bus.
`opus` conflated two axes: decoding Ogg-Opus, and vendoring the C library
to do it. So the only way to avoid a cmake build of libopus was to give up
Opus playback -- the wrong trade for Nix, which already ships one.
The adapter crate draws the line already: symphonia-adapter-libopus own
`bundled` feature is what pulls opusic-sys/bundled and with it cmake. So
declare the dependency `default-features = false` and add
`opus-bundled = ["symphonia-adapter-libopus?/bundled"]`, forwarded up
through crabidy-server and cbd. It stays in every `default`, so a plain
cargo build still needs nothing installed; opting out is the packager act.
The weak `?/` is load-bearing -- a plain `/bundled` would enable the
optional dependency itself, and `opus-bundled` would quietly become a
second "do we decode Opus" flag.
The flake native build then drops cmake and takes libopus from nixpkgs;
`headlessFeatures` already omitted opus-bundled, so it opts out for free.
The cross build keeps the vendored copy: it links statically, and an
unbundled -lopus would need a static aarch64 libopus staged for the target
the way alsa-lib is.
That exposed an older bug. rustc stamps no RUNPATH, and the -L from
buildInputs arrives through NIX_LDFLAGS, which ld-wrapper does not mirror
into the binary -- so the package linked cleanly and then refused to start.
RUNPATH was empty, meaning libasound.so.2 had never resolved either: the
package always depended on the caller having it on LD_LIBRARY_PATH, which
this repo dev shell happens to set. autoPatchelfHook now fills the RUNPATH
from buildInputs (plus stdenv.cc.cc for libgcc_s, the compiler own
unwinding runtime, which no crate declares) and fails the build on anything
it cannot resolve. Verified by running each binary under `env -i`, and with
LD_BIND_NOW=1 so every opus symbol binds eagerly.
Also: devenv sets OPUS_LIB_DIR, without which the unbundled build dies as
"mold: fatal: library not found: opus"; check-features *builds* the two
libopus variants rather than clippy-ing them, since clippy links nothing and
cannot tell a resolvable -lopus from a missing one; and cbd was missing an
`rss` pass-through, so the bundle could not select that provider alone.
Requested alongside this: the spectrum shadows fall over 10 seconds instead
of 4. The config default and SpectrumStyle::default are two spellings of
one thing, so the config test now asserts the whole resolved style equals
SpectrumStyle::default() rather than field-by-field.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sweep of docs/ and every README against the code, after several features
landed since the last one.
Mechanical checks, which find what reading does not: SUMMARY covers every
page and no more; every relative link resolves (docs/src/providers.md
pointed a directory above the book, twice); every #anchor matches a real
heading; every provider crate's Settings field is documented in both its
README and its book page; the feature table matches Cargo.toml, which it
did not -- `rss` was missing and "seven provider features" is now eight.
Prose that predated recent work: the root README's client-config sample
knew only `spectrum` and still claimed every option has a flag; its
spectrum and web-client sections predated the colors, segments, shadows,
pane tabs, register and seek; cbd-web/README.md likewise; intro.md's
provider tree was missing /rss; clients.md omitted volume and mute from
what the update stream carries; rssdy/README.md did not mention that only
audio enclosures become episodes.
The audit also found a defect the docs were right about: `Seek` was never
added to `minimum_role`, so it fell through to the owner-only default
while auth.md and architecture/roles-auth.md both promise a queue-owner
may control playback. With auth configured a queue-owner could play, skip
and change the volume, but got PermissionDenied on `,`/`.`. Seek now sits
with the other playback verbs.
The test meant to prevent that -- "a new RPC must be added to exactly one
list" -- compared the role lists against a hardcoded 24, so a 25th method
kept the suite green. It now reads the method names out of crabidy.proto
and compares sets: a count copied out of a file is not a check against it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
flake.nix first: its native package passed a bare --no-default-features,
which used to mean "everything but web-ui" and now means *no providers
at all*. It names its set explicitly
(all-providers,opus,spectrum,notifications); the aarch64 cross build
keeps the full defaults and its staged wasm bundle.
docs/src/build-features.md: the feature table with what each one costs to
lose, why fs takes /crabidy, /orphans, queue persistence and scan with
it, the opus/libopus build note, two worked examples, what is
deliberately not gated, and check-features. Linked from SUMMARY.md, and
config.md now says the providers list can only offer what the binary was
built with. README gains a short pointer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>