Skip to content

Commit 43b238c

Browse files
committed
fix: override MprisService to properly handle dbus errors
1 parent 5890c46 commit 43b238c

File tree

3 files changed

+125
-39
lines changed

3 files changed

+125
-39
lines changed

aiosendspin_mpris/adapter.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@
2424
if sys.platform == "linux":
2525
try:
2626
from mpris_api.adapter.IMprisAdapterPlayer import IMprisAdapterPlayer
27+
from mpris_api.adapter.IMprisAdapterPlayLists import IMprisAdapterPlayLists
2728
from mpris_api.adapter.IMprisAdapterRoot import IMprisAdapterRoot
2829
from mpris_api.adapter.IMprisAdapterTrackList import IMprisAdapterTrackList
2930
from mpris_api.common.DbusObject import DbusObject
3031
from mpris_api.model.MprisLoopStatus import MprisLoopStatus
3132
from mpris_api.model.MprisMetaData import MprisMetaData
3233
from mpris_api.model.MprisPlaybackStatus import MprisPlaybackStatus
34+
from mpris_api.model.MprisPlaylist import MprisPlaylist
35+
from mpris_api.model.MprisPlaylistOrdering import MprisPlaylistOrdering
3336
from tunit.unit import Microseconds
3437

3538
MPRIS_AVAILABLE = True # pyright: ignore[reportConstantRedefinition]
@@ -56,10 +59,17 @@ class _DummyIMprisAdapterTrackList:
5659
def __init__(self) -> None:
5760
pass
5861

62+
class _DummyIMprisAdapterPlayLists:
63+
"""Dummy adapter base class when mpris_api is not installed."""
64+
65+
def __init__(self) -> None:
66+
pass
67+
5968
if not TYPE_CHECKING: # otherwise pyright complains too much
6069
IMprisAdapterRoot = _DummyIMprisAdapterRoot
6170
IMprisAdapterPlayer = _DummyIMprisAdapterPlayer
6271
IMprisAdapterTrackList = _DummyIMprisAdapterTrackList
72+
IMprisAdapterPlayLists = _DummyIMprisAdapterPlayLists
6373

6474

6575
@dataclass
@@ -350,3 +360,33 @@ def canEditTracks(self) -> bool:
350360
def getTracks(self) -> list[DbusObject]:
351361
"""Return list of tracks (empty - not supported)."""
352362
return []
363+
364+
365+
class SendspinMprisAdapterPlaylists(IMprisAdapterPlayLists):
366+
"""Stub adapter for the MPRIS PlayLists interface.
367+
368+
This provides an empty implementation to prevent D-Bus errors when
369+
clients query for the PlayLists interface.
370+
"""
371+
372+
@override
373+
def getPlaylistCount(self) -> int:
374+
return 0
375+
376+
@override
377+
def getAvailableOrderings(self) -> list[MprisPlaylistOrdering]:
378+
return []
379+
380+
@override
381+
def getActivePlaylist(self) -> MprisPlaylist | None:
382+
return None
383+
384+
@override
385+
def activatePlaylist(self, playlistId: str) -> None:
386+
pass
387+
388+
@override
389+
def getPlaylists(
390+
self, index: int, maxCount: int, order: MprisPlaylistOrdering, reverseOrder: bool
391+
) -> list[MprisPlaylist]:
392+
return []

aiosendspin_mpris/mpris.py

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
import asyncio
66
import logging
77
from collections.abc import Callable
8-
from typing import TYPE_CHECKING, final, override
8+
from typing import TYPE_CHECKING
99

1010
from .adapter import (
1111
MPRIS_AVAILABLE,
1212
MprisState,
1313
SendspinMprisAdapterPlayer,
14+
SendspinMprisAdapterPlaylists,
1415
SendspinMprisAdapterRoot,
1516
SendspinMprisAdapterTrackList,
1617
)
@@ -22,34 +23,8 @@
2223
from mpris_api.MprisService import MprisService
2324
from mpris_api.MprisUpdateNotifier import MprisUpdateNotifier
2425

25-
_LOGGER = logging.getLogger(__name__)
26-
27-
if MPRIS_AVAILABLE:
28-
from mpris_api.MprisService import MprisService
29-
30-
31-
@final
32-
class _MprisErrorFilter(logging.Filter):
33-
"""Filter to suppress expected MPRIS interface errors.
34-
35-
Some MPRIS clients query for optional interfaces (like Playlists) which is not
36-
properly supported by the mpris-api lib. This filter suppresses the
37-
resulting D-Bus errors to avoid cluttering the output.
38-
"""
3926

40-
_SUPPRESSED_INTERFACES = ("org.mpris.MediaPlayer2.Playlists",)
41-
42-
@override
43-
def filter(self, record: logging.LogRecord) -> bool:
44-
"""Return False to suppress the log record, True to allow it."""
45-
if record.levelno != logging.ERROR:
46-
return True
47-
48-
msg = record.getMessage()
49-
if "could not find an interface" not in msg:
50-
return True
51-
52-
return not any(iface in msg for iface in self._SUPPRESSED_INTERFACES)
27+
_LOGGER = logging.getLogger(__name__)
5328

5429

5530
class SendspinMpris:
@@ -83,11 +58,11 @@ class SendspinMpris:
8358
_adapter_root: SendspinMprisAdapterRoot | None
8459
_adapter_player: SendspinMprisAdapterPlayer | None
8560
_adapter_tracklist: SendspinMprisAdapterTrackList | None
61+
_adapter_playlists: SendspinMprisAdapterPlaylists | None
8662
_service: MprisService | None
8763
_update_notifier: MprisUpdateNotifier | None
8864
_running: bool
8965
_listener_removers: list[Callable[[], None]]
90-
_error_filter: _MprisErrorFilter | None
9166

9267
def __init__(
9368
self, client: SendspinClient, name: str = "Sendspin", desktop_entry: str | None = None
@@ -110,11 +85,11 @@ def __init__(
11085
self._adapter_root = None
11186
self._adapter_player = None
11287
self._adapter_tracklist = None
88+
self._adapter_playlists = None
11389
self._service = None
11490
self._update_notifier = None
11591
self._running = False
11692
self._listener_removers = []
117-
self._error_filter = None
11893

11994
def start(self) -> None:
12095
"""Start the MPRIS D-Bus service and attach listeners to the client.
@@ -130,6 +105,8 @@ def start(self) -> None:
130105
_LOGGER.debug("MPRIS not available: mpris_api package not installed or not on Linux")
131106
return
132107

108+
from .mpris_service import PatchedMprisService
109+
133110
if self._running:
134111
_LOGGER.debug("MPRIS interface already running")
135112
return
@@ -139,21 +116,19 @@ def start(self) -> None:
139116
except RuntimeError as err:
140117
raise RuntimeError("MPRIS must be started from within a running event loop") from err
141118

142-
# Add filter to suppress expected D-Bus errors for unimplemented interfaces
143-
self._error_filter = _MprisErrorFilter()
144-
logging.getLogger().addFilter(self._error_filter)
145-
146119
self._adapter_root = SendspinMprisAdapterRoot(
147120
identity=self._name, desktop_entry=self._desktop_entry
148121
)
149122
self._adapter_player = SendspinMprisAdapterPlayer(self._client, self._loop, self._state)
150123
self._adapter_tracklist = SendspinMprisAdapterTrackList()
124+
self._adapter_playlists = SendspinMprisAdapterPlaylists()
151125

152-
self._service = MprisService(
126+
self._service = PatchedMprisService(
153127
name=self._name,
154128
adapterRoot=self._adapter_root,
155129
adapterPlayer=self._adapter_player,
156130
adapterTrackList=self._adapter_tracklist,
131+
adapterPlayLists=self._adapter_playlists,
157132
)
158133
self._service.start()
159134
self._update_notifier = self._service.updateNotifier
@@ -189,10 +164,7 @@ def stop(self) -> None:
189164
self._adapter_root = None
190165
self._adapter_player = None
191166
self._adapter_tracklist = None
192-
193-
if self._error_filter is not None:
194-
logging.getLogger().removeFilter(self._error_filter)
195-
self._error_filter = None
167+
self._adapter_playlists = None
196168

197169
_LOGGER.info("MPRIS interface stopped")
198170

aiosendspin_mpris/mpris_service.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Patched MPRIS service."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from typing import Any, override
7+
8+
from dbus_next.aio.message_bus import MessageBus
9+
from dbus_next.errors import InvalidAddressError
10+
from mpris_api.adapter.IMprisAdapterPlayer import IMprisAdapterPlayer
11+
from mpris_api.adapter.IMprisAdapterPlayLists import IMprisAdapterPlayLists
12+
from mpris_api.adapter.IMprisAdapterRoot import IMprisAdapterRoot
13+
from mpris_api.adapter.IMprisAdapterTrackList import IMprisAdapterTrackList
14+
from mpris_api.interface.MprisInterfacePlayLists import MprisInterfacePlayLists
15+
from mpris_api.model.MprisConstant import MprisConstant
16+
from mpris_api.MprisService import MprisService
17+
18+
_LOGGER = logging.getLogger(__name__)
19+
20+
21+
class PatchedMprisInterfacePlayLists(MprisInterfacePlayLists):
22+
"""Patched MprisInterfacePlayLists to return lists instead of tuples for D-Bus STRUCT types."""
23+
24+
@override
25+
def activePlaylist(self) -> list[Any]: # pyright: ignore[reportExplicitAny, reportIncompatibleMethodOverride]
26+
"""Return active playlist as a list.
27+
28+
The upstream definition seems to be wrong since DBus requires a list, not tuple.
29+
"""
30+
return []
31+
32+
33+
class PatchedMprisService(MprisService):
34+
"""Patched MprisService to handle InvalidAddressError gracefully."""
35+
36+
def __init__( # noqa: D107
37+
self,
38+
name: str,
39+
adapterRoot: IMprisAdapterRoot, # noqa: N803
40+
adapterPlayer: IMprisAdapterPlayer, # noqa: N803
41+
adapterTrackList: IMprisAdapterTrackList, # noqa: N803
42+
adapterPlayLists: IMprisAdapterPlayLists, # noqa: N803
43+
) -> None:
44+
super().__init__(
45+
name=name,
46+
adapterRoot=adapterRoot,
47+
adapterPlayer=adapterPlayer,
48+
adapterTrackList=adapterTrackList,
49+
adapterPlayLists=adapterPlayLists,
50+
)
51+
self._interfacePlayLists: MprisInterfacePlayLists | None = PatchedMprisInterfacePlayLists(
52+
adapter=adapterPlayLists
53+
)
54+
55+
@override
56+
async def _loop(self) -> None:
57+
try:
58+
self._messageBus = messageBus = await MessageBus().connect() # noqa: N806 # pyright: ignore[reportUnannotatedClassAttribute]
59+
60+
messageBus.export(MprisConstant.PATH, self._interfaceRoot)
61+
messageBus.export(MprisConstant.PATH, self._interfacePlayer)
62+
if self._interfaceTrackList:
63+
messageBus.export(MprisConstant.PATH, self._interfaceTrackList)
64+
if self._interfacePlayLists:
65+
messageBus.export(MprisConstant.PATH, self._interfacePlayLists)
66+
67+
_ = await messageBus.request_name(self._name)
68+
69+
await messageBus.wait_for_disconnect()
70+
71+
except InvalidAddressError:
72+
_LOGGER.warning("MPRIS not available: DBus address error")
73+
finally:
74+
self._disconnect()

0 commit comments

Comments
 (0)