Skip to content

Commit 9f23e6a

Browse files
committed
feat!: automatically subscribe to client
1 parent ff4fc0a commit 9f23e6a

File tree

2 files changed

+86
-11
lines changed

2 files changed

+86
-11
lines changed

aiosendspin_mpris/mpris.py

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
import asyncio
66
import logging
77
import threading
8+
from collections.abc import Callable
89
from typing import TYPE_CHECKING
910

1011
from .adapter import MPRIS_AVAILABLE, MprisState, SendspinMprisAdapter
1112

1213
if TYPE_CHECKING:
1314
from aiosendspin.client import SendspinClient
15+
from aiosendspin.models.core import GroupUpdateServerPayload, ServerStatePayload
1416
from aiosendspin.models.types import MediaCommand, PlaybackStateType
1517

1618
_LOGGER = logging.getLogger(__name__)
@@ -26,19 +28,18 @@ class SendspinMpris:
2628
2729
Provides desktop media control integration on Linux systems, using MPRIS.
2830
31+
When started, this class automatically registers listeners on the provided
32+
SendspinClient to update MPRIS state when metadata, playback state, or volume
33+
changes are received from the server.
34+
2935
Example usage:
3036
```python
3137
from aiosendspin import SendspinClient
3238
from aiosendspin_mpris import SendspinMpris, MPRIS_AVAILABLE
3339
3440
client = SendspinClient(...)
3541
mpris = SendspinMpris(client)
36-
mpris.start()
37-
38-
# Update state when it changes
39-
mpris.set_metadata(title="Song", artist="Artist", album="Album")
40-
mpris.set_playback_state(PlaybackStateType.PLAYING)
41-
mpris.set_volume(volume=75, muted=False)
42+
mpris.start() # Starts MPRIS and attaches listeners to client
4243
4344
# Later
4445
mpris.stop()
@@ -55,14 +56,15 @@ class SendspinMpris:
5556
_event_adapter: EventAdapter | None
5657
_thread: threading.Thread | None
5758
_running: bool
59+
_listener_removers: list[Callable[[], None]]
5860

5961
def __init__(
6062
self, client: SendspinClient, name: str = "Sendspin", desktop_entry: str | None = None
6163
) -> None:
6264
"""Initialize the MPRIS interface.
6365
6466
Args:
65-
client: SendspinClient instance for sending commands.
67+
client: SendspinClient instance for sending commands and receiving state updates.
6668
name: Application name shown in MPRIS (default: "Sendspin").
6769
desktop_entry: The .desktop file name, with the '.desktop' extension stripped, or None.
6870
@@ -79,11 +81,15 @@ def __init__(
7981
self._event_adapter = None
8082
self._thread = None
8183
self._running = False
84+
self._listener_removers = []
8285

8386
def start(self) -> None:
84-
"""Start the MPRIS D-Bus service.
87+
"""Start the MPRIS D-Bus service and attach listeners to the client.
88+
89+
This creates a background thread that runs the MPRIS server and registers
90+
listeners on the client to automatically update MPRIS state when metadata,
91+
playback state, or volume changes are received from the server.
8592
86-
This creates a background thread that runs the MPRIS server.
8793
If MPRIS is not available (not on Linux or mpris_server not installed),
8894
this method does nothing.
8995
"""
@@ -125,16 +131,26 @@ def run_loop() -> None:
125131
self._thread = threading.Thread(target=run_loop, daemon=True, name="mpris-server")
126132
self._thread.start()
127133
self._running = True
134+
135+
self._attach_client_listeners()
136+
128137
_LOGGER.info("MPRIS interface started")
129138

130139
def stop(self) -> None:
131-
"""Stop the MPRIS D-Bus service."""
140+
"""Stop the MPRIS D-Bus service and remove client listeners."""
132141
if not self._running:
133142
return
134143

135144
self._running = False
136145
self._event_adapter = None
137146

147+
for remover in self._listener_removers:
148+
try:
149+
remover()
150+
except Exception:
151+
_LOGGER.debug("Error removing listener", exc_info=True)
152+
self._listener_removers.clear()
153+
138154
if self._server is not None:
139155
try:
140156
self._server.quit()
@@ -149,6 +165,65 @@ def stop(self) -> None:
149165

150166
_LOGGER.info("MPRIS interface stopped")
151167

168+
def _attach_client_listeners(self) -> None:
169+
"""Attach event listeners to the client for MPRIS state updates."""
170+
self._listener_removers.append(self._client.add_metadata_listener(self._on_metadata_update))
171+
self._listener_removers.append(
172+
self._client.add_group_update_listener(self._on_group_update)
173+
)
174+
self._listener_removers.append(
175+
self._client.add_controller_state_listener(self._on_controller_state)
176+
)
177+
178+
_LOGGER.debug("Attached MPRIS listeners to SendspinClient")
179+
180+
def _on_metadata_update(self, payload: ServerStatePayload) -> None:
181+
"""Handle metadata updates from the client."""
182+
from aiosendspin.models.types import UndefinedField
183+
184+
metadata = payload.metadata
185+
if metadata is None:
186+
return
187+
188+
# Extract metadata fields
189+
title = (
190+
metadata.title if not isinstance(metadata.title, UndefinedField) else self._state.title
191+
)
192+
artist = (
193+
metadata.artist
194+
if not isinstance(metadata.artist, UndefinedField)
195+
else self._state.artist
196+
)
197+
album = (
198+
metadata.album if not isinstance(metadata.album, UndefinedField) else self._state.album
199+
)
200+
201+
# Extract duration from progress if available
202+
duration_ms = self._state.duration_ms
203+
progress_ms = self._state.progress_ms
204+
205+
if not isinstance(metadata.progress, UndefinedField) and metadata.progress is not None:
206+
duration_ms = metadata.progress.track_duration
207+
progress_ms = metadata.progress.track_progress
208+
209+
self.set_metadata(title=title, artist=artist, album=album, duration_ms=duration_ms)
210+
211+
if progress_ms is not None:
212+
self.set_progress(progress_ms)
213+
214+
def _on_group_update(self, payload: GroupUpdateServerPayload) -> None:
215+
"""Handle group update (playback state) from the client."""
216+
if payload.playback_state is not None:
217+
self.set_playback_state(payload.playback_state)
218+
219+
def _on_controller_state(self, payload: ServerStatePayload) -> None:
220+
"""Handle controller state updates from the client."""
221+
controller = payload.controller
222+
if controller is None:
223+
return
224+
self.set_supported_commands(set(controller.supported_commands))
225+
self.set_volume(controller.volume, muted=controller.muted)
226+
152227
def set_metadata(
153228
self,
154229
title: str | None = None,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ classifiers = [
1414
"Programming Language :: Python :: 3",
1515
]
1616
dependencies = [
17-
"aiosendspin>=1.2.0",
17+
"aiosendspin>=2.0.0",
1818
"mpris_server>=0.9.0; sys_platform == 'linux'",
1919
]
2020
license = { text = "Apache-2.0" }

0 commit comments

Comments
 (0)