feat(media): MprisController fängt Player- und Bus-Fehler sauber ab

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 18:58:42 +02:00
parent f23ee1249d
commit b5d6ae6ecc
2 changed files with 99 additions and 14 deletions
+76
View File
@@ -97,3 +97,79 @@ async def test_resume_with_empty_paused_list_is_noop(monkeypatch):
await controller.resume() await controller.resume()
get_player.assert_not_awaited() get_player.assert_not_awaited()
@pytest.mark.asyncio
async def test_pause_logs_and_continues_when_single_player_fails(monkeypatch, caplog):
from whisper_local.media._mpris import MprisController
good = _make_player("Playing")
async def fake_get_player(name: str):
if name.endswith("broken"):
raise RuntimeError("player disappeared")
return good
controller = MprisController()
monkeypatch.setattr(
controller,
"_list_player_names",
AsyncMock(return_value=[
"org.mpris.MediaPlayer2.broken",
"org.mpris.MediaPlayer2.good",
]),
)
monkeypatch.setattr(controller, "_get_player_interface", fake_get_player)
with caplog.at_level("WARNING"):
await controller.pause()
good.call_pause.assert_awaited_once()
assert controller._paused == ["org.mpris.MediaPlayer2.good"]
assert any("broken" in r.message for r in caplog.records)
@pytest.mark.asyncio
async def test_resume_logs_and_continues_when_single_player_fails(monkeypatch, caplog):
from whisper_local.media._mpris import MprisController
good = _make_player("Paused")
async def fake_get_player(name: str):
if name.endswith("broken"):
raise RuntimeError("player disappeared")
return good
controller = MprisController()
controller._paused = [
"org.mpris.MediaPlayer2.broken",
"org.mpris.MediaPlayer2.good",
]
monkeypatch.setattr(controller, "_get_player_interface", fake_get_player)
with caplog.at_level("WARNING"):
await controller.resume()
good.call_play.assert_awaited_once()
assert controller._paused == []
assert any("broken" in r.message for r in caplog.records)
@pytest.mark.asyncio
async def test_pause_is_noop_when_bus_unreachable(monkeypatch, caplog):
from whisper_local.media._mpris import MprisController
async def failing_connect(self):
raise RuntimeError("no session bus")
monkeypatch.setattr(
"dbus_next.aio.MessageBus.connect", failing_connect, raising=False
)
controller = MprisController()
with caplog.at_level("WARNING"):
await controller.pause()
assert controller._paused == []
assert any("D-Bus" in r.message or "bus" in r.message.lower()
for r in caplog.records)
+23 -14
View File
@@ -43,27 +43,36 @@ class MprisController:
"""Pausiert einen Player, wenn er im Status 'Playing' ist. """Pausiert einen Player, wenn er im Status 'Playing' ist.
Gibt den Bus-Namen zurück, wenn pausiert wurde, sonst None. Gibt den Bus-Namen zurück, wenn pausiert wurde, sonst None.
""" """
player = await self._get_player_interface(bus_name) try:
status = await player.get_playback_status() player = await self._get_player_interface(bus_name)
if status != "Playing": status = await player.get_playback_status()
if status != "Playing":
return None
await player.call_pause()
return bus_name
except Exception as e:
logger.warning("Konnte Player %s nicht pausieren: %s", bus_name, e)
return None return None
await player.call_pause()
return bus_name async def _resume_player(self, bus_name: str) -> None:
try:
player = await self._get_player_interface(bus_name)
await player.call_play()
except Exception as e:
logger.warning("Konnte Player %s nicht fortsetzen: %s", bus_name, e)
async def pause(self) -> None: async def pause(self) -> None:
names = await self._list_player_names() try:
names = await self._list_player_names()
except Exception as e:
logger.warning("D-Bus nicht erreichbar, überspringe Media-Pause: %s", e)
self._paused = []
return
results = await asyncio.gather( results = await asyncio.gather(
*(self._pause_player(n) for n in names), *(self._pause_player(n) for n in names),
return_exceptions=True, return_exceptions=True,
) )
self._paused = [ self._paused = [name for name in results if isinstance(name, str)]
name for name in results
if isinstance(name, str)
]
async def _resume_player(self, bus_name: str) -> None:
player = await self._get_player_interface(bus_name)
await player.call_play()
async def resume(self) -> None: async def resume(self) -> None:
if not self._paused: if not self._paused: