From 60f95c94f77cfa794099979ded7373212762978a Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Tue, 18 Aug 2026 16:20:03 +0100 Subject: [PATCH] fix: drop the tolerant playlist patch from the media harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _TolerantPlaylist rebound ovos_media.player.Playlist to hide a bare-string constructor arg quirk. Against current ovos-media (>=2.0.0a9) the player's internal queue is its own PlayQueue, so the patch's only remaining effect is making a genuine ovos_utils Playlist fail the module-global isinstance check in set_now_playing. Delete the shim and bump the media/dev extras' ovos-media floor to 2.0.0a9 (the pre-restructure 0.0.2a3 pin was stale and never exercised by this harness code). The same restructure dropped BaseMediaService's validate_source parameter — session-source gating moved to the player's bus edge — so the harness's real- backend injection path (OCPPlayerHarness.__enter__, ovoscope/media.py) drops the stale kwarg too; nothing in the harness read AudioService.validate_source, so there was no behavior to replace. Co-Authored-By: Claude Fable 5 --- ovoscope/media.py | 33 +-------------------------------- pyproject.toml | 8 ++++++-- test/unittests/test_media.py | 22 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 34 deletions(-) diff --git a/ovoscope/media.py b/ovoscope/media.py index fe7b0a3..c8c6b4f 100644 --- a/ovoscope/media.py +++ b/ovoscope/media.py @@ -260,9 +260,6 @@ class OCPPlayerHarness: - ``ovos_media.player.VideoService`` - ``ovos_media.player.WebService`` - ``ovos_media.player.OcpMprisExporter`` - - ``ovos_media.player.GUIInterface`` (exposed as ``harness.gui``, when - the installed ``ovos-media`` still defines it — builds that have - dropped in-core GUI integration skip this patch) - ``ovos_media.player.OCPMediaCatalog`` - ``ovos_media.player.Configuration`` (returns ``{"media": {}}``) @@ -346,30 +343,12 @@ def __enter__(self) -> "OCPPlayerHarness": gui_mock = MagicMock() self.gui = gui_mock - # Patch Playlist so that Playlist("Search Results") doesn't try to - # add the string as a media entry. The installed ovos_utils.ocp.Playlist - # treats all positional args as entries; we need a subclass that silently - # drops bare string args (which are titles, not entries) and still - # satisfies isinstance(x, Playlist) checks inside player.py. - from ovos_utils.ocp import Playlist as _RealPlaylist - - class _TolerantPlaylist(_RealPlaylist): - """Playlist subclass that ignores bare string constructor args.""" - - def __init__(self, *args, **kwargs): - valid = [a for a in args if not isinstance(a, str)] - super().__init__(*valid, **kwargs) - # Every mock.patch below is process-wide until stopped. If anything # after the first start() raises (a missing ovos_media attribute, a # backend constructor error), an unguarded exit would leave those # patches active for the rest of the process and silently corrupt # every later test. Unwind through __exit__ before propagating. try: - p_playlist = patch("ovos_media.player.Playlist", _TolerantPlaylist) - p_playlist.start() - self._patches.append(p_playlist) - simple_targets = [ "ovos_media.player.AudioService", "ovos_media.player.VideoService", @@ -387,16 +366,6 @@ def __init__(self, *args, **kwargs): p_cfg.start() self._patches.append(p_cfg) - # GUIInterface only exists on ovos-media builds that still carry - # in-core GUI integration; newer builds have dropped the symbol - # entirely, so patching it unconditionally would AttributeError. - import ovos_media.player as _ocp_player_module - if hasattr(_ocp_player_module, "GUIInterface"): - p_gui = patch("ovos_media.player.GUIInterface", - return_value=gui_mock) - p_gui.start() - self._patches.append(p_gui) - # Instantiate the real player (all heavy deps are now mocked) self.player = OCPMediaPlayer(self.bus, config={}) @@ -409,7 +378,7 @@ def __init__(self, *args, **kwargs): # a Music Assistant client's play_media() call). from ovos_media.media_backends.audio import AudioService as _RealAudioService audio_svc = _RealAudioService(self.bus, config={"audio_players": {}}, - autoload=False, validate_source=False) + autoload=False) self.player.audio_service = audio_svc # Deferred uris (e.g. library://, {sei}//) are resolved by the OCP # pipeline's stream extractors *before* the player sees them; this diff --git a/pyproject.toml b/pyproject.toml index d27b5a1..f8d7f9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,11 @@ pydantic = ["ovos-pydantic-models>=0.1.0"] # ovos-spec-tools 0.10.0a1, so that is the real floor. audio = ["ovos-audio>=1.3.0a1", "ovos-spec-tools>=0.10.0a1"] # OCP / ovos-media player harnesses (ovoscope.media: OCPPlayerHarness, ...). -media = ["ovos-media>=0.0.2a3"] +# 2.0.0a9 floor: the harness targets the restructured daemon layout +# (media_backends.audio.AudioService/BaseMediaService signatures, no in-core +# GUI); older ovos-media has a different module layout the harness never +# supported in practice. +media = ["ovos-media>=2.0.0a9"] # MiniListener plugin_instances path (feed_audio_stream) needs the dinkum # AudioTransformersService. >=0.7.2a1 is the first release that allows # ovos-bus-client 2.x (older pins cap it <2.0.0 and conflict with ovos-core). @@ -64,7 +68,7 @@ tts = [ ] dev = [ "ovos-audio>=1.3.0a1", - "ovos-media>=0.0.2a3", + "ovos-media>=2.0.0a9", "ovos-dinkum-listener>=0.7.2a1", "ovos-pydantic-models>=0.1.0", "numpy", diff --git a/test/unittests/test_media.py b/test/unittests/test_media.py index 62e050d..1067bcd 100644 --- a/test/unittests/test_media.py +++ b/test/unittests/test_media.py @@ -300,6 +300,28 @@ def test_player_drives_injected_backend_play(self) -> None: assert h.backend.play_calls == ["library://track/42"] +@pytest.mark.skipif(not _HAS_OVOS_MEDIA, + reason="requires the [media] extra (ovos-media)") +class TestOCPHarnessRealPlaylistIsinstance: + """A genuine ovos_utils.ocp.Playlist must satisfy the isinstance checks + inside ovos_media.player.set_now_playing when driven through the harness. + + A harness that swaps ``ovos_media.player.Playlist`` for a local subclass + would make this fail: set_now_playing does `isinstance(track, Playlist)` + against the *module-global* name, so a caller passing a real Playlist + would be rejected as neither a MediaEntry nor a Playlist. + """ + + def test_real_playlist_passes_isinstance_in_set_now_playing(self) -> None: + from ovos_utils.ocp import MediaEntry, Playlist, PlaybackType + + with OCPPlayerHarness() as h: + entry = MediaEntry(uri="library://track/1", playback=PlaybackType.AUDIO) + playlist = Playlist(entry) + h.player.set_now_playing(playlist) + assert h.player.now_playing.uri == "library://track/1" + + # --------------------------------------------------------------------------- # Namespace bridging # ---------------------------------------------------------------------------