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:
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user