Snapcast MPRIS bridge
Project description
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-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 mute = pause-all integration were dropped.
Install
From the Odio APT repository (recommended)
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 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.
Signals
SIGUSR1—Stream.Control Pauseon the bound stream.SIGUSR2—Stream.Control Stopon 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,mypyandpytest. - 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-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.deb, sdist and wheel to the GitHub release, flagging-rc/-beta/-alphatags 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/-alphatags stop at TestPyPI. - notify-apt-repo on
v*tags — dispatches tob0bbywan/odio-apt-reposo the new.debis picked up byapt.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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file snapclientmpris-1.2.1.tar.gz.
File metadata
- Download URL: snapclientmpris-1.2.1.tar.gz
- Upload date:
- Size: 29.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe6c6671988babfec34f68537254a13ad1235b8791295d4b44aaabff1b2d95e1
|
|
| MD5 |
8805f430f5aadd15c0a6ad998dbae6d0
|
|
| BLAKE2b-256 |
0fd1894864e5671a19a83ca6dd730bed3468957c728a7bba82d069942ca5cc34
|
Provenance
The following attestation bundles were made for snapclientmpris-1.2.1.tar.gz:
Publisher:
build.yml on b0bbywan/snapclientmpris
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
snapclientmpris-1.2.1.tar.gz -
Subject digest:
fe6c6671988babfec34f68537254a13ad1235b8791295d4b44aaabff1b2d95e1 - Sigstore transparency entry: 1829061368
- Sigstore integration time:
-
Permalink:
b0bbywan/snapclientmpris@c8f5f79dee885bebdf8edd0af3f5cda507988f20 -
Branch / Tag:
refs/tags/v1.2.1 - Owner: https://github.com/b0bbywan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@c8f5f79dee885bebdf8edd0af3f5cda507988f20 -
Trigger Event:
push
-
Statement type:
File details
Details for the file snapclientmpris-1.2.1-py3-none-any.whl.
File metadata
- Download URL: snapclientmpris-1.2.1-py3-none-any.whl
- Upload date:
- Size: 20.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
946c1d34378bc7f0fbad4892711829447665e7901e158b5a7e44f75c3120e1c2
|
|
| MD5 |
51611846f5652f2c1aeea39f8526b4b2
|
|
| BLAKE2b-256 |
910e480d515f9d7d18adb8cc69fbf9766804ab5409f1eec501c3d832f56d362d
|
Provenance
The following attestation bundles were made for snapclientmpris-1.2.1-py3-none-any.whl:
Publisher:
build.yml on b0bbywan/snapclientmpris
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
snapclientmpris-1.2.1-py3-none-any.whl -
Subject digest:
946c1d34378bc7f0fbad4892711829447665e7901e158b5a7e44f75c3120e1c2 - Sigstore transparency entry: 1829061372
- Sigstore integration time:
-
Permalink:
b0bbywan/snapclientmpris@c8f5f79dee885bebdf8edd0af3f5cda507988f20 -
Branch / Tag:
refs/tags/v1.2.1 - Owner: https://github.com/b0bbywan
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
build.yml@c8f5f79dee885bebdf8edd0af3f5cda507988f20 -
Trigger Event:
push
-
Statement type: