Skip to main content

snapclientmpris

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).

Credits

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.

The current codebase is a complete rewrite around asyncio. The repository was subsequently renamed fromsnapcastmpris to snapclientmpris to better reflect what the daemon does.

What's different from upstream

  • Single asyncio event loop instead of threads + GLib MainLoop + websocket-client + dbus-python.
  • python-snapcast for the snapserver JSON-RPC channel (no bespoke RPC / WebSocket client) and dbus-fast for the MPRIS interface (no GLib).
  • Picks up track metadata from the Stream.OnProperties snapserver event (snapserver ≥ 0.27) and surfaces it as xesam:* / 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.Control rather 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.
  • Position is the position the stream's source reports, advanced locally while playing, and Seek / SetPosition are forwarded to the source when it can seek. The source only reports its position now and then (meta_mpd.py: on every MPD event and every 15 seconds), so right after connecting or changing stream it can be that far off until the next report, which emits Seeked.
  • Configuration is resolved from $XDG_CONFIG_HOME/snapclientmpris/snapclientmpris.conf with /etc/snapclientmpris.conf as fallback. An example template ships at /usr/share/snapclientmpris/snapclientmpris.conf.
  • The dbus-bus config key chooses between the session bus (default, for a systemctl --user deployment) and the system bus (legacy hifiberry-style, runs as _snapclient with a shipped D-Bus policy).
  • The ALSA volume sync and the mute = pause-all integration were dropped.

Install

The .deb is the turn-key route: it wires up the systemd units, the D-Bus policy, the config template and the snapclient dependency.

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 snapclientmpris

The package depends on snapclient, so APT pulls it in automatically. Two bridge units are shipped (neither auto-enabled); pick whichever fits your setup. Both use Type=dbus with BusName=org.mpris.MediaPlayer2.snapcast and pull in 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.service

In user mode the bridge is also D-Bus session-activatable: any MPRIS client (playerctl, desktop media keys, gnome-music) requesting the bus name starts it on demand, so enable --now is only needed if you want it up before any client asks for it.

In 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).

From PyPI

Final releases are published to PyPI, prereleases to TestPyPI:

pipx install snapclientmpris        # or: pip install --user snapclientmpris

The PyPI distribution ships only the snapclientmpris daemon, not the systemd units, D-Bus policy or config template the .deb installs. The daemon runs without a config file (Zeroconf auto-discovery), and snapclient itself still has to come from your distro. To run it under systemd, drop in a user unit pointing at the pipx/pip binary:

mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/snapclientmpris.service <<'EOF'
[Unit]
Description=Snapcast MPRIS2 bridge
After=network-online.target snapclient.service
Wants=network-online.target snapclient.service

[Service]
Type=dbus
BusName=org.mpris.MediaPlayer2.snapcast
ExecStart=%h/.local/bin/snapclientmpris
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
EOF

systemctl --user daemon-reload
systemctl --user enable --now snapclientmpris.service

Adjust ExecStart if snapclientmpris lives elsewhere (which snapclientmpris). Unlike the APT install there is no D-Bus session activation, so the unit (or a manual foreground run) is what starts the bridge.

Configuration

# 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

Usage

Normally the daemon is started by systemd (see Installation). Run it directly for debugging:

snapclientmpris -v          # run in the foreground with debug logging
snapclientmpris --discover  # probe the network for Snapcast services and exit

--discover performs a one-shot Zeroconf lookup and prints the resolved IP and port of the snapserver control socket and the snapweb UI, without starting the daemon:

snapserver:  tcp://192.168.1.21:1705
snapweb:     http://192.168.1.21:1780

(IPv4 only. A snapserver < 0.33 that advertises only _snapcast._tcp shows up as snapserver: tcp://<ip> without a port.)

Architecture

  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 the run() 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 single refresh() that re-publishes PlaybackStatus, Metadata, Volume and capabilities.
  • snapclientmpris/mpris.py — MediaPlayer2 and MediaPlayer2.Player ServiceInterface subclasses for dbus-fast (D-Bus interface definitions only).
  • snapclientmpris/translate.py — pure helpers that map snapserver's MPRIS-like metadata to xesam:* / mpris:* keys and snapserver stream state to an MPRIS PlaybackStatus. No D-Bus or asyncio dependencies, so fully unit-testable in isolation.

Signals

  • SIGUSR1 — Stream.Control Pause on the bound stream.
  • SIGUSR2 — Stream.Control Stop on the bound stream.

For inspecting the running bridge, the MPRIS bus and the snapserver JSON-RPC channel, see DEBUGGING.md.

Development

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__.py

snapclientmpris/__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 a .deb

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.

Continuous integration

.github/workflows/build.yml runs:

  • lint on every PR to master — ruff, mypy and pytest.
  • build on every PR and on v* tags — make build (sdist + wheel), uploaded as an artifact for the release and publish jobs.
  • deb on every PR and on v* tags — dpkg-buildpackage inside a debian:trixie container; on tags, syncs debian/changelog with the tag (rewriting -rc/-beta/-alpha to Debian-sortable ~rc/... suffixes) before building.
  • release on v* tags — attaches the .deb, sdist and wheel to the GitHub release, flagging -rc/-beta/-alpha tags as prereleases.
  • publish-to-testpypi on v* tags — uploads sdist + wheel to TestPyPI via trusted publishing (all tags, prereleases included).
  • publish-to-pypi on final v* tags only — uploads to PyPI via trusted publishing; -rc/-beta/-alpha tags stop at TestPyPI.
  • notify-apt-repo on v* tags — dispatches to b0bbywan/odio-apt-repo so the new .deb is picked up by apt.odio.love.

Used in

  • 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.

License

MIT — see LICENSE.

Metadata

Release files for snapclientmpris 1.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for snapclientmpris 1.3.0
File Size Uploaded
snapclientmpris-1.3.0.tar.gz 33.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for snapclientmpris 1.3.0
File Interpreter ABI Platform
snapclientmpris-1.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 56.0 kB

Release files / snapclientmpris-1.3.0.tar.gz

Download URL snapclientmpris-1.3.0.tar.gz
Size 33.9 kB
Tags Source
SHA-256 checksum
How to use checksums
b66b9fa727c61845dc5726778b5e7faa84de9c816350f5be396d7852d1d8d98c
BLAKE2b-256 checksum
How to use checksums
7fbc36116556693f7e469b090f0ef02361d13f6372f61ecc04249b75ff1ef951
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release files / snapclientmpris-1.3.0-py3-none-any.whl

Download URL snapclientmpris-1.3.0-py3-none-any.whl
Size 22.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
39294bc9b05cd02af222fd3112f66354f66bb460b4a300450376122bafa7c2a0
BLAKE2b-256 checksum
How to use checksums
59888c12cb708a1f1f0c1b16158274945585a4ce403e3b235b8ec1ceecbb3b71
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 11, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.3.0 This release

2 release files

1.2.2

2 release files

1.2.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page