From 89edf23de91a949c2e0b486d4a64817ee8fd020e Mon Sep 17 00:00:00 2001 From: Vitali Graf Date: Wed, 15 Apr 2026 20:14:36 +0200 Subject: [PATCH] feat(media): SmtcController.pause() erkennt und pausiert PLAYING-Sessions --- tests/test_media_smtc.py | 76 ++++++++++++++++++++++++++++++++++++ whisper_local/media/_smtc.py | 30 +++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/tests/test_media_smtc.py b/tests/test_media_smtc.py index dc733dd..656485f 100644 --- a/tests/test_media_smtc.py +++ b/tests/test_media_smtc.py @@ -73,3 +73,79 @@ async def test_pause_skips_reconnect_after_smtc_failure(monkeypatch): await controller.pause() assert call_count == 1 + + +@pytest.mark.asyncio +async def test_pause_with_no_sessions_is_noop(monkeypatch): + from whisper_local.media._smtc import SmtcController + + controller = SmtcController() + monkeypatch.setattr( + controller, "_ensure_manager", AsyncMock(return_value=_make_manager([])) + ) + + await controller.pause() + + assert controller._paused == [] + + +@pytest.mark.asyncio +async def test_pause_pauses_all_playing_sessions(monkeypatch): + from whisper_local.media._smtc import SmtcController + + s1 = _make_session("Spotify", PLAYING) + s2 = _make_session("msedge", PLAYING) + controller = SmtcController() + monkeypatch.setattr( + controller, + "_ensure_manager", + AsyncMock(return_value=_make_manager([s1, s2])), + ) + + await controller.pause() + + s1.try_pause_async.assert_awaited_once() + s2.try_pause_async.assert_awaited_once() + assert controller._paused == ["Spotify", "msedge"] + + +@pytest.mark.asyncio +async def test_pause_skips_already_paused_sessions(monkeypatch): + from whisper_local.media._smtc import SmtcController + + playing = _make_session("Spotify", PLAYING) + already_paused = _make_session("msedge", PAUSED) + controller = SmtcController() + monkeypatch.setattr( + controller, + "_ensure_manager", + AsyncMock(return_value=_make_manager([playing, already_paused])), + ) + + await controller.pause() + + playing.try_pause_async.assert_awaited_once() + already_paused.try_pause_async.assert_not_awaited() + assert controller._paused == ["Spotify"] + + +@pytest.mark.asyncio +async def test_pause_logs_and_continues_when_session_fails(monkeypatch, caplog): + from whisper_local.media._smtc import SmtcController + + broken = _make_session("broken", PLAYING) + broken.try_pause_async = AsyncMock(side_effect=RuntimeError("Verbindung verloren")) + good = _make_session("Spotify", PLAYING) + controller = SmtcController() + monkeypatch.setattr( + controller, + "_ensure_manager", + AsyncMock(return_value=_make_manager([broken, good])), + ) + + with caplog.at_level("WARNING"): + await controller.pause() + + good.try_pause_async.assert_awaited_once() + assert controller._paused == ["Spotify"] + assert any("broken" in r.message for r in caplog.records) diff --git a/whisper_local/media/_smtc.py b/whisper_local/media/_smtc.py index 4dd3b26..82d3fa3 100644 --- a/whisper_local/media/_smtc.py +++ b/whisper_local/media/_smtc.py @@ -24,12 +24,32 @@ class SmtcController: ) return self._manager + async def _pause_session(self, session: Any) -> str | None: + """Pausiert eine Session wenn sie spielt. Gibt AUMID zurück, sonst None.""" + from winrt.windows.media.control import ( + GlobalSystemMediaTransportControlsSessionPlaybackStatus, + ) + aumid = session.source_app_user_model_id + try: + info = session.get_playback_info() + if ( + info.playback_status + != GlobalSystemMediaTransportControlsSessionPlaybackStatus.PLAYING + ): + return None + await session.try_pause_async() + return aumid + except Exception as e: + logger.warning("Konnte Session %s nicht pausieren: %s", aumid, e) + return None + async def pause(self) -> None: if self._broken: self._paused = [] return + try: - await self._ensure_manager() + manager = await self._ensure_manager() except Exception as e: logger.warning( "SMTC nicht erreichbar, Media-Pause dauerhaft deaktiviert: %s", e @@ -38,5 +58,13 @@ class SmtcController: self._paused = [] return + sessions = list(manager.get_sessions()) + paused = [] + for session in sessions: + result = await self._pause_session(session) + if result is not None: + paused.append(result) + self._paused = paused + async def resume(self) -> None: pass