An MPRIS2 D-Bus
bridge for the local Snapcast client. It surfaces the currently playing
track (title, artist, album, art) from a snapserver and forwards MPRIS
playback commands (Play / Pause / PlayPause / Stop / Next / Previous) to
the stream's source via snapserver's Stream.Control — so pausing from
any room pauses every listener on the stream, the multi-room semantic
MPRIS expects (à la Spotify Connect / Airplay 2).
The MPRIS interface is published under the bus name
org.mpris.MediaPlayer2.snapcast (the player exposes itself as the
Snapcast source, not the client implementation detail).
This project started life as a fork of
hifiberry/snapcastmpris
— thanks to HiFiBerry for the original idea and for the work on tying
Snapcast's JSON-RPC API to MPRIS2.
Without their daemon there would be nothing to fork.
The current codebase is a complete rewrite around asyncio and contains
no upstream code. The repository was subsequently renamed from
snapcastmpris to snapclientmpris to better reflect what the daemon
does — it controls the local snapclient process, not the snapserver.
- Single asyncio event loop instead of threads + GLib MainLoop + websocket-client + dbus-python.
python-snapcastfor the snapserver JSON-RPC channel (no bespoke RPC / WebSocket client) anddbus-fastfor the MPRIS interface (no GLib).- Picks up track metadata from the
Stream.OnPropertiessnapserver event (snapserver ≥ 0.27) and surfaces it asxesam:*/mpris:*keys, so MPRIS clients see the actual track title / artist / album. - MPRIS Play / Pause / Next / Previous / Stop are forwarded to the
stream's source via
Stream.Controlrather than toggling the local client's mute, so pausing from one room pauses everyone on the stream. Capabilities (CanPlay/CanPause/CanGoNext/CanGoPrevious/CanSeek) are mirrored from the stream's properties, so MPRIS clients only enable the buttons the source actually supports. - Configuration is resolved from
$XDG_CONFIG_HOME/snapclientmpris/snapclientmpris.confwith/etc/snapclientmpris.confas fallback. An example template ships at/usr/share/snapclientmpris/snapclientmpris.conf. - The
dbus-busconfig key chooses between the session bus (default, for asystemctl --userdeployment) and the system bus (legacy hifiberry-style, runs as_snapclientwith a shipped D-Bus policy). - The ALSA volume sync and the HiFiBerry pause-all integration are intentionally dropped; they were tied to the original HiFiBerry appliance and don't fit the Odio target.
From the Odio APT repository:
curl -fsSL https://apt.odio.love/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/odio.gpg
echo "deb [signed-by=/usr/share/keyrings/odio.gpg] https://apt.odio.love stable main" \
| sudo tee /etc/apt/sources.list.d/odio.list
sudo apt update
sudo apt install snapclientmprisThe package depends on snapclient, so APT pulls it in automatically.
Two bridge units are shipped (neither auto-enabled); pick whichever
fits your setup — enabling the bridge unit also starts
snapclient.service via Wants=.
# User mode (default, session bus)
systemctl --user enable --now snapclientmpris.service
# System mode (legacy hifiberry-style, runs as _snapclient on the system bus)
sudo cp /usr/share/snapclientmpris/snapclientmpris.conf /etc/snapclientmpris.conf
sudo sed -i 's/^dbus-bus = session/dbus-bus = system/' /etc/snapclientmpris.conf
sudo systemctl enable --now snapclientmpris.serviceIn system mode the daemon owns org.mpris.MediaPlayer2.snapcast on
the system bus; the package ships the matching D-Bus policy at
/usr/share/dbus-1/system.d/org.mpris.MediaPlayer2.snapcast.conf
(grants _snapclient ownership, allows any local user to talk to it).
# Snapcast server IP. Leave commented to use Zeroconf auto-discovery.
# server = 192.168.1.100
# Override the JSON-RPC control port. Almost never needed: snapserver
# defaults to 1705, and snapserver >= 0.33 advertises the actual port via
# _snapcast-ctrl._tcp. Only useful if you've changed snapserver's TCP
# control port AND you run snapserver < 0.33 (e.g. 0.31 in Debian trixie).
# control-port = 1705
# D-Bus bus: session (default) or system.
dbus-bus = session
remote local host
---------- ------------------------------------
audio +-------------+ +----------+
+---------------+ ---------------> | snapclient | -> | speakers |
| snapserver | | (own unit) | +----------+
| | +-------------+
| JSON-RPC :1705| <-- python-snapcast --+
+---------------+ (control + events) |
v
+---------------------------+
| snapclientmpris daemon |
| (this package, asyncio) |
+-------------+-------------+
| D-Bus (dbus-fast)
v
+---------------------------+
| MPRIS2 clients |
| (gnome-music, playerctl) |
+---------------------------+
The daemon does not spawn snapclient. Snapclient runs as its own
service (snapclient.service from the snapclient Debian package);
the shipped systemd units pull it in via Wants=snapclient.service
and order After=snapclient.service, so enabling
snapclientmpris.service is enough.
Four Python modules:
snapclientmpris/cli.py— entry point. Parses CLI flags, loads the config file, resolves the snapserver address (explicit value or Zeroconf discovery), then hands off to therun()coroutine.snapclientmpris/snapclientmpris.py— asyncio orchestration. Connects to the snapserver, matches this host to its snapserver-side client by MAC, exports the MPRIS interface, and wires the snapserver stream/client callbacks to a singlerefresh()that re-publishes PlaybackStatus, Metadata, Volume and capabilities.snapclientmpris/mpris.py—MediaPlayer2andMediaPlayer2.PlayerServiceInterfacesubclasses for dbus-fast (D-Bus interface definitions only).snapclientmpris/translate.py— pure helpers that map snapserver's MPRIS-like metadata toxesam:*/mpris:*keys and snapserver stream state to an MPRISPlaybackStatus. No D-Bus or asyncio dependencies, so fully unit-testable in isolation.
SIGUSR1—Stream.Control Pauseon the bound stream.SIGUSR2—Stream.Control Stopon the bound stream.
A top-level Makefile wraps the day-to-day commands so local dev and
CI stay in sync (the GitHub workflow calls the same targets):
make lint # ruff + mypy
make test # pytest
make build # python -m build (sdist + wheel)
make deb # dpkg-buildpackage -b -us -uc (Debian toolchain)
make clean # drop build/, dist/, *.egg-info
make version # print the Python version (from __init__.py)
make sync-deb # bump debian/changelog to match __init__.pysnapclientmpris/__init__.py is the single source of truth for the
version; make sync-deb and make check-tag TAG=… keep
debian/changelog and the git tag aligned with it.
Build-deps (per debian/control): debhelper-compat (= 13),
dh-python, python3, python3-setuptools. Then make deb on
Debian trixie or a derivative produces the .deb (wraps
dpkg-buildpackage -b -us -uc). The runtime deps
(python3-snapcast, python3-dbus-fast, python3-zeroconf,
snapclient) are resolved by APT at install time, not at build time.
.github/workflows/build.yml runs:
- lint on every PR to
master—ruff,mypyandpytest. - deb on every PR and on
v*tags —dpkg-buildpackageinside adebian:trixiecontainer; on tags, syncsdebian/changelogwith the tag (rewriting-rc/-beta/-alphato Debian-sortable~rc/...suffixes) before building. - release on
v*tags — attaches the.debto the GitHub release, flagging-rc/-beta/-alphatags as prereleases. - notify-apt-repo on
v*tags — dispatches tob0bbywan/odio-apt-reposo the new.debis picked up byapt.odio.love.
- Odio — the Odio streamer installer turns a Linux box (typically a Raspberry Pi) into a multi-room audio appliance; snapclientmpris is its per-room MPRIS layer on top of snapcast.
MIT — see LICENSE.