Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 1 addition & 32 deletions ovoscope/media.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}}``)

Expand Down Expand Up @@ -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",
Expand All @@ -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={})

Expand All @@ -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
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions test/unittests/test_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down
Loading