snapclientmpris 1.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019-2025 HiFiBerry <info@hifiberry.com>
4
+ Copyright (c) 2026 Mathieu Réquillart <mathieu.requillart@gmail.com>
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,314 @@
1
+ Metadata-Version: 2.4
2
+ Name: snapclientmpris
3
+ Version: 1.2.1
4
+ Summary: Snapcast MPRIS bridge
5
+ Author-email: Mathieu Réquillart <mathieu.requillart@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/b0bbywan/snapclientmpris
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: POSIX :: Linux
10
+ Requires-Python: >=3.11
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: snapcast>=2.3
14
+ Requires-Dist: dbus-fast>=2.0
15
+ Requires-Dist: zeroconf>=0.28
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest; extra == "dev"
18
+ Requires-Dist: mypy; extra == "dev"
19
+ Requires-Dist: ruff; extra == "dev"
20
+ Dynamic: license-file
21
+
22
+ # snapclientmpris
23
+
24
+ An [MPRIS2](https://specifications.freedesktop.org/mpris-spec/2.2/) D-Bus
25
+ bridge for the local Snapcast client. It surfaces the currently playing
26
+ track (title, artist, album, art) from a snapserver and forwards MPRIS
27
+ playback commands (Play / Pause / PlayPause / Stop / Next / Previous) to
28
+ the stream's source via snapserver's `Stream.Control` — so pausing from
29
+ any room pauses every listener on the stream, the multi-room semantic
30
+ MPRIS expects (à la Spotify Connect / Airplay 2).
31
+
32
+ The MPRIS interface is published under the bus name
33
+ `org.mpris.MediaPlayer2.snapcast` (the player exposes itself as the
34
+ Snapcast source, not the client implementation detail).
35
+
36
+ ## Credits
37
+
38
+ This project started life as a fork of
39
+ [`hifiberry/snapcastmpris`](https://github.com/hifiberry/snapcastmpris)
40
+ — thanks to HiFiBerry for the original idea and for the work on tying
41
+ [Snapcast](https://github.com/snapcast/snapcast)'s JSON-RPC API to MPRIS2.
42
+
43
+ The current codebase is a complete rewrite around asyncio.
44
+ The repository was subsequently renamed from`snapcastmpris`
45
+ to `snapclientmpris` to better reflect what the daemon does.
46
+
47
+ ## What's different from upstream
48
+
49
+ * Single asyncio event loop instead of threads + GLib MainLoop +
50
+ websocket-client + dbus-python.
51
+ * [`python-snapcast`](https://github.com/happyleavesaoc/python-snapcast)
52
+ for the snapserver JSON-RPC channel (no bespoke RPC / WebSocket
53
+ client) and [`dbus-fast`](https://github.com/Bluetooth-Devices/dbus-fast)
54
+ for the MPRIS interface (no GLib).
55
+ * Picks up track metadata from the `Stream.OnProperties` snapserver
56
+ event (snapserver ≥ 0.27) and surfaces it as `xesam:*` / `mpris:*`
57
+ keys, so MPRIS clients see the actual track title / artist / album.
58
+ * MPRIS Play / Pause / Next / Previous / Stop are forwarded to the
59
+ stream's source via `Stream.Control` rather than toggling the local
60
+ client's mute, so pausing from one room pauses everyone on the
61
+ stream. Capabilities (`CanPlay` / `CanPause` / `CanGoNext` /
62
+ `CanGoPrevious` / `CanSeek`) are mirrored from the stream's
63
+ properties, so MPRIS clients only enable the buttons the source
64
+ actually supports.
65
+ * Configuration is resolved from
66
+ `$XDG_CONFIG_HOME/snapclientmpris/snapclientmpris.conf` with
67
+ `/etc/snapclientmpris.conf` as fallback. An example template ships at
68
+ `/usr/share/snapclientmpris/snapclientmpris.conf`.
69
+ * The `dbus-bus` config key chooses between the session bus (default,
70
+ for a `systemctl --user` deployment) and the system bus (legacy
71
+ hifiberry-style, runs as `_snapclient` with a shipped D-Bus policy).
72
+ * The ALSA volume sync and the mute = pause-all integration were dropped.
73
+
74
+ ## Install
75
+
76
+ ### From the Odio APT repository (recommended)
77
+
78
+ The `.deb` is the turn-key route: it wires up the systemd units, the
79
+ D-Bus policy, the config template and the `snapclient` dependency.
80
+
81
+ ```sh
82
+ curl -fsSL https://apt.odio.love/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/odio.gpg
83
+ echo "deb [signed-by=/usr/share/keyrings/odio.gpg] https://apt.odio.love stable main" \
84
+ | sudo tee /etc/apt/sources.list.d/odio.list
85
+ sudo apt update
86
+ sudo apt install snapclientmpris
87
+ ```
88
+
89
+ The package depends on `snapclient`, so APT pulls it in automatically.
90
+ Two bridge units are shipped (neither auto-enabled); pick whichever
91
+ fits your setup.
92
+ Both use `Type=dbus` with `BusName=org.mpris.MediaPlayer2.snapcast`
93
+ and pull in `snapclient.service` via `Wants=`.
94
+
95
+ ```sh
96
+ # User mode (default, session bus)
97
+ systemctl --user enable --now snapclientmpris.service
98
+
99
+ # System mode (legacy hifiberry-style, runs as _snapclient on the system bus)
100
+ sudo cp /usr/share/snapclientmpris/snapclientmpris.conf /etc/snapclientmpris.conf
101
+ sudo sed -i 's/^dbus-bus = session/dbus-bus = system/' /etc/snapclientmpris.conf
102
+ sudo systemctl enable --now snapclientmpris.service
103
+ ```
104
+
105
+ In user mode the bridge is also D-Bus session-activatable: any MPRIS
106
+ client (`playerctl`, desktop media keys, gnome-music) requesting the
107
+ bus name starts it on demand, so `enable --now` is only needed if you
108
+ want it up before any client asks for it.
109
+
110
+ In system mode the daemon owns `org.mpris.MediaPlayer2.snapcast` on
111
+ the system bus; the package ships the matching D-Bus policy at
112
+ `/usr/share/dbus-1/system.d/org.mpris.MediaPlayer2.snapcast.conf`
113
+ (grants `_snapclient` ownership, allows any local user to talk to it).
114
+
115
+ ### From PyPI
116
+
117
+ Final releases are published to [PyPI](https://pypi.org/p/snapclientmpris),
118
+ prereleases to [TestPyPI](https://test.pypi.org/p/snapclientmpris):
119
+
120
+ ```sh
121
+ pipx install snapclientmpris # or: pip install --user snapclientmpris
122
+ ```
123
+
124
+ The PyPI distribution ships **only** the `snapclientmpris` daemon, not
125
+ the systemd units, D-Bus policy or config template the `.deb` installs.
126
+ The daemon runs without a config file (Zeroconf auto-discovery), and
127
+ `snapclient` itself still has to come from your distro. To run it under
128
+ systemd, drop in a user unit pointing at the pipx/pip binary:
129
+
130
+ ```sh
131
+ mkdir -p ~/.config/systemd/user
132
+ cat > ~/.config/systemd/user/snapclientmpris.service <<'EOF'
133
+ [Unit]
134
+ Description=Snapcast MPRIS2 bridge
135
+ After=network-online.target snapclient.service
136
+ Wants=network-online.target snapclient.service
137
+
138
+ [Service]
139
+ Type=dbus
140
+ BusName=org.mpris.MediaPlayer2.snapcast
141
+ ExecStart=%h/.local/bin/snapclientmpris
142
+ Restart=on-failure
143
+ RestartSec=5
144
+
145
+ [Install]
146
+ WantedBy=default.target
147
+ EOF
148
+
149
+ systemctl --user daemon-reload
150
+ systemctl --user enable --now snapclientmpris.service
151
+ ```
152
+
153
+ Adjust `ExecStart` if `snapclientmpris` lives elsewhere (`which
154
+ snapclientmpris`). Unlike the APT install there is no D-Bus session
155
+ activation, so the unit (or a manual foreground run) is what starts the
156
+ bridge.
157
+
158
+ ## Configuration
159
+
160
+ ```ini
161
+ # Snapcast server IP. Leave commented to use Zeroconf auto-discovery.
162
+ # server = 192.168.1.100
163
+ # Override the JSON-RPC control port. Almost never needed: snapserver
164
+ # defaults to 1705, and snapserver >= 0.33 advertises the actual port via
165
+ # _snapcast-ctrl._tcp. Only useful if you've changed snapserver's TCP
166
+ # control port AND you run snapserver < 0.33 (e.g. 0.31 in Debian trixie).
167
+ # control-port = 1705
168
+
169
+ # D-Bus bus: session (default) or system.
170
+ dbus-bus = session
171
+
172
+ ```
173
+
174
+ ## Usage
175
+
176
+ Normally the daemon is started by systemd (see Installation). Run it
177
+ directly for debugging:
178
+
179
+ ```sh
180
+ snapclientmpris -v # run in the foreground with debug logging
181
+ snapclientmpris --discover # probe the network for Snapcast services and exit
182
+ ```
183
+
184
+ `--discover` performs a one-shot Zeroconf lookup and prints the
185
+ resolved IP and port of the snapserver control socket and the snapweb
186
+ UI, without starting the daemon:
187
+
188
+ ```
189
+ snapserver: tcp://192.168.1.21:1705
190
+ snapweb: http://192.168.1.21:1780
191
+ ```
192
+
193
+ (IPv4 only. A snapserver < 0.33 that advertises only `_snapcast._tcp`
194
+ shows up as `snapserver: tcp://<ip>` without a port.)
195
+
196
+ ## Architecture
197
+
198
+ ```
199
+ remote local host
200
+ ---------- ------------------------------------
201
+
202
+ audio +-------------+ +----------+
203
+ +---------------+ ---------------> | snapclient | -> | speakers |
204
+ | snapserver | | (own unit) | +----------+
205
+ | | +-------------+
206
+ | JSON-RPC :1705| <-- python-snapcast --+
207
+ +---------------+ (control + events) |
208
+ v
209
+ +---------------------------+
210
+ | snapclientmpris daemon |
211
+ | (this package, asyncio) |
212
+ +-------------+-------------+
213
+ | D-Bus (dbus-fast)
214
+ v
215
+ +---------------------------+
216
+ | MPRIS2 clients |
217
+ | (gnome-music, playerctl) |
218
+ +---------------------------+
219
+ ```
220
+
221
+ The daemon does **not** spawn snapclient. Snapclient runs as its own
222
+ service (`snapclient.service` from the `snapclient` Debian package);
223
+ the shipped systemd units pull it in via `Wants=snapclient.service`
224
+ and order `After=snapclient.service`, so enabling
225
+ `snapclientmpris.service` is enough.
226
+
227
+ Four Python modules:
228
+
229
+ * [`snapclientmpris/cli.py`](snapclientmpris/cli.py) — entry point.
230
+ Parses CLI flags, loads the config file, resolves the snapserver
231
+ address (explicit value or Zeroconf discovery), then hands off to
232
+ the `run()` coroutine.
233
+ * [`snapclientmpris/snapclientmpris.py`](snapclientmpris/snapclientmpris.py)
234
+ — asyncio orchestration. Connects to the snapserver, matches this
235
+ host to its snapserver-side client by MAC, exports the MPRIS
236
+ interface, and wires the snapserver stream/client callbacks to a
237
+ single `refresh()` that re-publishes PlaybackStatus, Metadata,
238
+ Volume and capabilities.
239
+ * [`snapclientmpris/mpris.py`](snapclientmpris/mpris.py) —
240
+ `MediaPlayer2` and `MediaPlayer2.Player` `ServiceInterface`
241
+ subclasses for dbus-fast (D-Bus interface definitions only).
242
+ * [`snapclientmpris/translate.py`](snapclientmpris/translate.py) —
243
+ pure helpers that map snapserver's MPRIS-like metadata to
244
+ `xesam:*` / `mpris:*` keys and snapserver stream state to an
245
+ MPRIS `PlaybackStatus`. No D-Bus or asyncio dependencies, so
246
+ fully unit-testable in isolation.
247
+
248
+ ## Signals
249
+
250
+ * `SIGUSR1` — `Stream.Control Pause` on the bound stream.
251
+ * `SIGUSR2` — `Stream.Control Stop` on the bound stream.
252
+
253
+ For inspecting the running bridge, the MPRIS bus and the snapserver
254
+ JSON-RPC channel, see [DEBUGGING.md](DEBUGGING.md).
255
+
256
+ ## Development
257
+
258
+ A top-level `Makefile` wraps the day-to-day commands so local dev and
259
+ CI stay in sync (the GitHub workflow calls the same targets):
260
+
261
+ ```sh
262
+ make lint # ruff + mypy
263
+ make test # pytest
264
+ make build # python -m build (sdist + wheel)
265
+ make deb # dpkg-buildpackage -b -us -uc (Debian toolchain)
266
+ make clean # drop build/, dist/, *.egg-info
267
+ make version # print the Python version (from __init__.py)
268
+ make sync-deb # bump debian/changelog to match __init__.py
269
+ ```
270
+
271
+ `snapclientmpris/__init__.py` is the single source of truth for the
272
+ version; `make sync-deb` and `make check-tag TAG=…` keep
273
+ `debian/changelog` and the git tag aligned with it.
274
+
275
+ ## Build a .deb
276
+
277
+ Build-deps (per `debian/control`): `debhelper-compat (= 13)`,
278
+ `dh-python`, `python3`, `python3-setuptools`. Then `make deb` on
279
+ Debian trixie or a derivative produces the `.deb` (wraps
280
+ `dpkg-buildpackage -b -us -uc`). The runtime deps
281
+ (`python3-snapcast`, `python3-dbus-fast`, `python3-zeroconf`,
282
+ `snapclient`) are resolved by APT at install time, not at build time.
283
+
284
+ ## Continuous integration
285
+
286
+ `.github/workflows/build.yml` runs:
287
+
288
+ * **lint** on every PR to `master` — `ruff`, `mypy` and `pytest`.
289
+ * **build** on every PR and on `v*` tags — `make build` (sdist +
290
+ wheel), uploaded as an artifact for the release and publish jobs.
291
+ * **deb** on every PR and on `v*` tags — `dpkg-buildpackage` inside a
292
+ `debian:trixie` container; on tags, syncs `debian/changelog` with
293
+ the tag (rewriting `-rc/-beta/-alpha` to Debian-sortable `~rc/...`
294
+ suffixes) before building.
295
+ * **release** on `v*` tags — attaches the `.deb`, sdist and wheel to
296
+ the GitHub release, flagging `-rc/-beta/-alpha` tags as prereleases.
297
+ * **publish-to-testpypi** on `v*` tags — uploads sdist + wheel to
298
+ TestPyPI via trusted publishing (all tags, prereleases included).
299
+ * **publish-to-pypi** on final `v*` tags only — uploads to PyPI via
300
+ trusted publishing; `-rc/-beta/-alpha` tags stop at TestPyPI.
301
+ * **notify-apt-repo** on `v*` tags — dispatches to
302
+ [`b0bbywan/odio-apt-repo`](https://github.com/b0bbywan/odio-apt-repo)
303
+ so the new `.deb` is picked up by `apt.odio.love`.
304
+
305
+ ## Used in
306
+
307
+ * [Odio](https://github.com/b0bbywan/odios) — the Odio streamer
308
+ installer turns a Linux box (typically a Raspberry Pi) into a
309
+ multi-room audio appliance; snapclientmpris is its per-room MPRIS
310
+ layer on top of snapcast.
311
+
312
+ ## License
313
+
314
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,293 @@
1
+ # snapclientmpris
2
+
3
+ An [MPRIS2](https://specifications.freedesktop.org/mpris-spec/2.2/) D-Bus
4
+ bridge for the local Snapcast client. It surfaces the currently playing
5
+ track (title, artist, album, art) from a snapserver and forwards MPRIS
6
+ playback commands (Play / Pause / PlayPause / Stop / Next / Previous) to
7
+ the stream's source via snapserver's `Stream.Control` — so pausing from
8
+ any room pauses every listener on the stream, the multi-room semantic
9
+ MPRIS expects (à la Spotify Connect / Airplay 2).
10
+
11
+ The MPRIS interface is published under the bus name
12
+ `org.mpris.MediaPlayer2.snapcast` (the player exposes itself as the
13
+ Snapcast source, not the client implementation detail).
14
+
15
+ ## Credits
16
+
17
+ This project started life as a fork of
18
+ [`hifiberry/snapcastmpris`](https://github.com/hifiberry/snapcastmpris)
19
+ — thanks to HiFiBerry for the original idea and for the work on tying
20
+ [Snapcast](https://github.com/snapcast/snapcast)'s JSON-RPC API to MPRIS2.
21
+
22
+ The current codebase is a complete rewrite around asyncio.
23
+ The repository was subsequently renamed from`snapcastmpris`
24
+ to `snapclientmpris` to better reflect what the daemon does.
25
+
26
+ ## What's different from upstream
27
+
28
+ * Single asyncio event loop instead of threads + GLib MainLoop +
29
+ websocket-client + dbus-python.
30
+ * [`python-snapcast`](https://github.com/happyleavesaoc/python-snapcast)
31
+ for the snapserver JSON-RPC channel (no bespoke RPC / WebSocket
32
+ client) and [`dbus-fast`](https://github.com/Bluetooth-Devices/dbus-fast)
33
+ for the MPRIS interface (no GLib).
34
+ * Picks up track metadata from the `Stream.OnProperties` snapserver
35
+ event (snapserver ≥ 0.27) and surfaces it as `xesam:*` / `mpris:*`
36
+ keys, so MPRIS clients see the actual track title / artist / album.
37
+ * MPRIS Play / Pause / Next / Previous / Stop are forwarded to the
38
+ stream's source via `Stream.Control` rather than toggling the local
39
+ client's mute, so pausing from one room pauses everyone on the
40
+ stream. Capabilities (`CanPlay` / `CanPause` / `CanGoNext` /
41
+ `CanGoPrevious` / `CanSeek`) are mirrored from the stream's
42
+ properties, so MPRIS clients only enable the buttons the source
43
+ actually supports.
44
+ * Configuration is resolved from
45
+ `$XDG_CONFIG_HOME/snapclientmpris/snapclientmpris.conf` with
46
+ `/etc/snapclientmpris.conf` as fallback. An example template ships at
47
+ `/usr/share/snapclientmpris/snapclientmpris.conf`.
48
+ * The `dbus-bus` config key chooses between the session bus (default,
49
+ for a `systemctl --user` deployment) and the system bus (legacy
50
+ hifiberry-style, runs as `_snapclient` with a shipped D-Bus policy).
51
+ * The ALSA volume sync and the mute = pause-all integration were dropped.
52
+
53
+ ## Install
54
+
55
+ ### From the Odio APT repository (recommended)
56
+
57
+ The `.deb` is the turn-key route: it wires up the systemd units, the
58
+ D-Bus policy, the config template and the `snapclient` dependency.
59
+
60
+ ```sh
61
+ curl -fsSL https://apt.odio.love/key.gpg | sudo gpg --dearmor -o /usr/share/keyrings/odio.gpg
62
+ echo "deb [signed-by=/usr/share/keyrings/odio.gpg] https://apt.odio.love stable main" \
63
+ | sudo tee /etc/apt/sources.list.d/odio.list
64
+ sudo apt update
65
+ sudo apt install snapclientmpris
66
+ ```
67
+
68
+ The package depends on `snapclient`, so APT pulls it in automatically.
69
+ Two bridge units are shipped (neither auto-enabled); pick whichever
70
+ fits your setup.
71
+ Both use `Type=dbus` with `BusName=org.mpris.MediaPlayer2.snapcast`
72
+ and pull in `snapclient.service` via `Wants=`.
73
+
74
+ ```sh
75
+ # User mode (default, session bus)
76
+ systemctl --user enable --now snapclientmpris.service
77
+
78
+ # System mode (legacy hifiberry-style, runs as _snapclient on the system bus)
79
+ sudo cp /usr/share/snapclientmpris/snapclientmpris.conf /etc/snapclientmpris.conf
80
+ sudo sed -i 's/^dbus-bus = session/dbus-bus = system/' /etc/snapclientmpris.conf
81
+ sudo systemctl enable --now snapclientmpris.service
82
+ ```
83
+
84
+ In user mode the bridge is also D-Bus session-activatable: any MPRIS
85
+ client (`playerctl`, desktop media keys, gnome-music) requesting the
86
+ bus name starts it on demand, so `enable --now` is only needed if you
87
+ want it up before any client asks for it.
88
+
89
+ In system mode the daemon owns `org.mpris.MediaPlayer2.snapcast` on
90
+ the system bus; the package ships the matching D-Bus policy at
91
+ `/usr/share/dbus-1/system.d/org.mpris.MediaPlayer2.snapcast.conf`
92
+ (grants `_snapclient` ownership, allows any local user to talk to it).
93
+
94
+ ### From PyPI
95
+
96
+ Final releases are published to [PyPI](https://pypi.org/p/snapclientmpris),
97
+ prereleases to [TestPyPI](https://test.pypi.org/p/snapclientmpris):
98
+
99
+ ```sh
100
+ pipx install snapclientmpris # or: pip install --user snapclientmpris
101
+ ```
102
+
103
+ The PyPI distribution ships **only** the `snapclientmpris` daemon, not
104
+ the systemd units, D-Bus policy or config template the `.deb` installs.
105
+ The daemon runs without a config file (Zeroconf auto-discovery), and
106
+ `snapclient` itself still has to come from your distro. To run it under
107
+ systemd, drop in a user unit pointing at the pipx/pip binary:
108
+
109
+ ```sh
110
+ mkdir -p ~/.config/systemd/user
111
+ cat > ~/.config/systemd/user/snapclientmpris.service <<'EOF'
112
+ [Unit]
113
+ Description=Snapcast MPRIS2 bridge
114
+ After=network-online.target snapclient.service
115
+ Wants=network-online.target snapclient.service
116
+
117
+ [Service]
118
+ Type=dbus
119
+ BusName=org.mpris.MediaPlayer2.snapcast
120
+ ExecStart=%h/.local/bin/snapclientmpris
121
+ Restart=on-failure
122
+ RestartSec=5
123
+
124
+ [Install]
125
+ WantedBy=default.target
126
+ EOF
127
+
128
+ systemctl --user daemon-reload
129
+ systemctl --user enable --now snapclientmpris.service
130
+ ```
131
+
132
+ Adjust `ExecStart` if `snapclientmpris` lives elsewhere (`which
133
+ snapclientmpris`). Unlike the APT install there is no D-Bus session
134
+ activation, so the unit (or a manual foreground run) is what starts the
135
+ bridge.
136
+
137
+ ## Configuration
138
+
139
+ ```ini
140
+ # Snapcast server IP. Leave commented to use Zeroconf auto-discovery.
141
+ # server = 192.168.1.100
142
+ # Override the JSON-RPC control port. Almost never needed: snapserver
143
+ # defaults to 1705, and snapserver >= 0.33 advertises the actual port via
144
+ # _snapcast-ctrl._tcp. Only useful if you've changed snapserver's TCP
145
+ # control port AND you run snapserver < 0.33 (e.g. 0.31 in Debian trixie).
146
+ # control-port = 1705
147
+
148
+ # D-Bus bus: session (default) or system.
149
+ dbus-bus = session
150
+
151
+ ```
152
+
153
+ ## Usage
154
+
155
+ Normally the daemon is started by systemd (see Installation). Run it
156
+ directly for debugging:
157
+
158
+ ```sh
159
+ snapclientmpris -v # run in the foreground with debug logging
160
+ snapclientmpris --discover # probe the network for Snapcast services and exit
161
+ ```
162
+
163
+ `--discover` performs a one-shot Zeroconf lookup and prints the
164
+ resolved IP and port of the snapserver control socket and the snapweb
165
+ UI, without starting the daemon:
166
+
167
+ ```
168
+ snapserver: tcp://192.168.1.21:1705
169
+ snapweb: http://192.168.1.21:1780
170
+ ```
171
+
172
+ (IPv4 only. A snapserver < 0.33 that advertises only `_snapcast._tcp`
173
+ shows up as `snapserver: tcp://<ip>` without a port.)
174
+
175
+ ## Architecture
176
+
177
+ ```
178
+ remote local host
179
+ ---------- ------------------------------------
180
+
181
+ audio +-------------+ +----------+
182
+ +---------------+ ---------------> | snapclient | -> | speakers |
183
+ | snapserver | | (own unit) | +----------+
184
+ | | +-------------+
185
+ | JSON-RPC :1705| <-- python-snapcast --+
186
+ +---------------+ (control + events) |
187
+ v
188
+ +---------------------------+
189
+ | snapclientmpris daemon |
190
+ | (this package, asyncio) |
191
+ +-------------+-------------+
192
+ | D-Bus (dbus-fast)
193
+ v
194
+ +---------------------------+
195
+ | MPRIS2 clients |
196
+ | (gnome-music, playerctl) |
197
+ +---------------------------+
198
+ ```
199
+
200
+ The daemon does **not** spawn snapclient. Snapclient runs as its own
201
+ service (`snapclient.service` from the `snapclient` Debian package);
202
+ the shipped systemd units pull it in via `Wants=snapclient.service`
203
+ and order `After=snapclient.service`, so enabling
204
+ `snapclientmpris.service` is enough.
205
+
206
+ Four Python modules:
207
+
208
+ * [`snapclientmpris/cli.py`](snapclientmpris/cli.py) — entry point.
209
+ Parses CLI flags, loads the config file, resolves the snapserver
210
+ address (explicit value or Zeroconf discovery), then hands off to
211
+ the `run()` coroutine.
212
+ * [`snapclientmpris/snapclientmpris.py`](snapclientmpris/snapclientmpris.py)
213
+ — asyncio orchestration. Connects to the snapserver, matches this
214
+ host to its snapserver-side client by MAC, exports the MPRIS
215
+ interface, and wires the snapserver stream/client callbacks to a
216
+ single `refresh()` that re-publishes PlaybackStatus, Metadata,
217
+ Volume and capabilities.
218
+ * [`snapclientmpris/mpris.py`](snapclientmpris/mpris.py) —
219
+ `MediaPlayer2` and `MediaPlayer2.Player` `ServiceInterface`
220
+ subclasses for dbus-fast (D-Bus interface definitions only).
221
+ * [`snapclientmpris/translate.py`](snapclientmpris/translate.py) —
222
+ pure helpers that map snapserver's MPRIS-like metadata to
223
+ `xesam:*` / `mpris:*` keys and snapserver stream state to an
224
+ MPRIS `PlaybackStatus`. No D-Bus or asyncio dependencies, so
225
+ fully unit-testable in isolation.
226
+
227
+ ## Signals
228
+
229
+ * `SIGUSR1` — `Stream.Control Pause` on the bound stream.
230
+ * `SIGUSR2` — `Stream.Control Stop` on the bound stream.
231
+
232
+ For inspecting the running bridge, the MPRIS bus and the snapserver
233
+ JSON-RPC channel, see [DEBUGGING.md](DEBUGGING.md).
234
+
235
+ ## Development
236
+
237
+ A top-level `Makefile` wraps the day-to-day commands so local dev and
238
+ CI stay in sync (the GitHub workflow calls the same targets):
239
+
240
+ ```sh
241
+ make lint # ruff + mypy
242
+ make test # pytest
243
+ make build # python -m build (sdist + wheel)
244
+ make deb # dpkg-buildpackage -b -us -uc (Debian toolchain)
245
+ make clean # drop build/, dist/, *.egg-info
246
+ make version # print the Python version (from __init__.py)
247
+ make sync-deb # bump debian/changelog to match __init__.py
248
+ ```
249
+
250
+ `snapclientmpris/__init__.py` is the single source of truth for the
251
+ version; `make sync-deb` and `make check-tag TAG=…` keep
252
+ `debian/changelog` and the git tag aligned with it.
253
+
254
+ ## Build a .deb
255
+
256
+ Build-deps (per `debian/control`): `debhelper-compat (= 13)`,
257
+ `dh-python`, `python3`, `python3-setuptools`. Then `make deb` on
258
+ Debian trixie or a derivative produces the `.deb` (wraps
259
+ `dpkg-buildpackage -b -us -uc`). The runtime deps
260
+ (`python3-snapcast`, `python3-dbus-fast`, `python3-zeroconf`,
261
+ `snapclient`) are resolved by APT at install time, not at build time.
262
+
263
+ ## Continuous integration
264
+
265
+ `.github/workflows/build.yml` runs:
266
+
267
+ * **lint** on every PR to `master` — `ruff`, `mypy` and `pytest`.
268
+ * **build** on every PR and on `v*` tags — `make build` (sdist +
269
+ wheel), uploaded as an artifact for the release and publish jobs.
270
+ * **deb** on every PR and on `v*` tags — `dpkg-buildpackage` inside a
271
+ `debian:trixie` container; on tags, syncs `debian/changelog` with
272
+ the tag (rewriting `-rc/-beta/-alpha` to Debian-sortable `~rc/...`
273
+ suffixes) before building.
274
+ * **release** on `v*` tags — attaches the `.deb`, sdist and wheel to
275
+ the GitHub release, flagging `-rc/-beta/-alpha` tags as prereleases.
276
+ * **publish-to-testpypi** on `v*` tags — uploads sdist + wheel to
277
+ TestPyPI via trusted publishing (all tags, prereleases included).
278
+ * **publish-to-pypi** on final `v*` tags only — uploads to PyPI via
279
+ trusted publishing; `-rc/-beta/-alpha` tags stop at TestPyPI.
280
+ * **notify-apt-repo** on `v*` tags — dispatches to
281
+ [`b0bbywan/odio-apt-repo`](https://github.com/b0bbywan/odio-apt-repo)
282
+ so the new `.deb` is picked up by `apt.odio.love`.
283
+
284
+ ## Used in
285
+
286
+ * [Odio](https://github.com/b0bbywan/odios) — the Odio streamer
287
+ installer turns a Linux box (typically a Raspberry Pi) into a
288
+ multi-room audio appliance; snapclientmpris is its per-room MPRIS
289
+ layer on top of snapcast.
290
+
291
+ ## License
292
+
293
+ MIT — see [LICENSE](LICENSE).