55import asyncio
66import logging
77import threading
8+ from collections .abc import Callable
89from typing import TYPE_CHECKING
910
1011from .adapter import MPRIS_AVAILABLE , MprisState , SendspinMprisAdapter
1112
1213if 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 ,
0 commit comments