From 218e06f9efc48d7c269b891b7b69bb1b6eb96b18 Mon Sep 17 00:00:00 2001 From: JarbasAI <33701864+JarbasAl@users.noreply.github.com> Date: Sun, 2 Jun 2024 22:12:57 +0100 Subject: [PATCH] default to OCP pipeline + support for utils 0.0.38 (#119) * remove dead code since https://github.com/OpenVoiceOS/ovos-core/pull/491 and https://github.com/OpenVoiceOS/ovos-config/pull/96 this is effectively dead code all NLP matching is dropped in this PR * rm dead code * drop old tests --- .github/workflows/unit_tests.yml | 4 + ovos_plugin_common_play/__init__.py | 6 +- ovos_plugin_common_play/launcher.py | 4 +- ovos_plugin_common_play/ocp/__init__.py | 17 +- ovos_plugin_common_play/ocp/base.py | 10 +- ovos_plugin_common_play/ocp/constants.py | 2 +- ovos_plugin_common_play/ocp/gui.py | 87 ++--- ovos_plugin_common_play/ocp/media.py | 360 ++++++++++++++++++++- ovos_plugin_common_play/ocp/mpris.py | 2 +- ovos_plugin_common_play/ocp/mycroft_cps.py | 4 +- ovos_plugin_common_play/ocp/player.py | 22 +- ovos_plugin_common_play/ocp/search.py | 9 +- ovos_plugin_common_play/ocp/status.py | 3 +- ovos_plugin_common_play/ocp/utils.py | 3 +- requirements/requirements.txt | 3 +- requirements/requirements_extra.txt | 1 + requirements/test.txt | 2 +- test/unittests/test_ocp.py | 5 +- test/unittests/test_ocp_media.py | 3 +- 19 files changed, 458 insertions(+), 89 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index d2575a2..5f7c38f 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -60,6 +60,10 @@ jobs: # NOTE: additional pytest invocations should also add the --cov-append flag # or they will overwrite previous invocations' coverage reports # (for an example, see OVOS Skill Manager's workflow) + - name: Run unittests with ovos-utils 0.0.38 + run: | + pip install ovos-utils==0.0.38 + pytest --cov=ovos_plugin_common_play --cov-report xml test/unittests - name: Upload coverage env: CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} diff --git a/ovos_plugin_common_play/__init__.py b/ovos_plugin_common_play/__init__.py index 1d1e67c..e18b864 100644 --- a/ovos_plugin_common_play/__init__.py +++ b/ovos_plugin_common_play/__init__.py @@ -1,10 +1,12 @@ from pprint import pformat from ovos_bus_client import Message +from ovos_utils.log import LOG +from ovos_workshop.decorators.ocp import * + from ovos_plugin_common_play.ocp import OCP from ovos_plugin_common_play.ocp.base import OCPAudioPlayerBackend -from ovos_utils.ocp import * -from ovos_utils.log import LOG +from ovos_plugin_common_play.ocp.constants import OCP_ID class OCPAudioBackend(OCPAudioPlayerBackend): diff --git a/ovos_plugin_common_play/launcher.py b/ovos_plugin_common_play/launcher.py index ee27c35..ea16a1c 100644 --- a/ovos_plugin_common_play/launcher.py +++ b/ovos_plugin_common_play/launcher.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 from ovos_bus_client import MessageBusClient -from ovos_plugin_common_play.ocp import OCP, OCP_ID from ovos_utils import wait_for_exit_signal +from ovos_plugin_common_play.ocp import OCP +from ovos_plugin_common_play.ocp.constants import OCP_ID + def main(): """ diff --git a/ovos_plugin_common_play/ocp/__init__.py b/ovos_plugin_common_play/ocp/__init__.py index 62ab527..fff1bfc 100644 --- a/ovos_plugin_common_play/ocp/__init__.py +++ b/ovos_plugin_common_play/ocp/__init__.py @@ -6,13 +6,15 @@ from ovos_utils.gui import can_use_gui from ovos_utils.log import LOG from ovos_utils.messagebus import Message -from ovos_utils.ocp import OCP_ID + from padacioso import IntentContainer from ovos_workshop import OVOSAbstractApplication from ovos_workshop.decorators.ocp import * from ovos_plugin_manager.ocp import load_stream_extractors +from ovos_plugin_common_play.ocp.constants import OCP_ID + class OCP(OVOSAbstractApplication): intent2media = { @@ -125,10 +127,15 @@ def handle_home(self, message=None): @property def using_new_pipeline(self) -> bool: - # TODO - default to True in ovos-core 0.1.0 - # more info: https://github.com/OpenVoiceOS/ovos-core/pull/456 - moved_to_pipelines = Configuration().get("intents", {}).get("experimental_ocp_pipeline") - return moved_to_pipelines + # this is no longer configurable, most of this repo is dead code + # keep this check to allow smooth updates from the couple alpha versions this was live + if Configuration().get("intents", {}).get("experimental_ocp_pipeline"): + return True + # check for min version for default ovos-config to contain OCP pipeline + from ovos_config.version import VERSION_BUILD, VERSION_ALPHA, VERSION_MAJOR, VERSION_MINOR + if VERSION_BUILD > 13 or VERSION_MAJOR >= 1 or VERSION_MINOR >= 1: + return True + return VERSION_BUILD == 13 and VERSION_ALPHA >= 14 def register_ocp_intents(self, message=None): if self.using_new_pipeline: diff --git a/ovos_plugin_common_play/ocp/base.py b/ovos_plugin_common_play/ocp/base.py index 1a6fe92..b97d2db 100644 --- a/ovos_plugin_common_play/ocp/base.py +++ b/ovos_plugin_common_play/ocp/base.py @@ -1,13 +1,13 @@ from os.path import join, isfile from ovos_bus_client.message import Message -from ovos_plugin_common_play.ocp.utils import extract_metadata -from ovos_utils.log import LOG -from ovos_utils.ocp import MediaState, PlayerState, OCP_ID, TrackState - - from ovos_config.locations import get_xdg_config_save_path from ovos_plugin_manager.templates.audio import AudioBackend +from ovos_utils.log import LOG +from ovos_workshop.decorators.ocp import MediaState, PlayerState, TrackState + +from ovos_plugin_common_play.ocp.constants import OCP_ID +from ovos_plugin_common_play.ocp.utils import extract_metadata class OCPAbstractComponent: diff --git a/ovos_plugin_common_play/ocp/constants.py b/ovos_plugin_common_play/ocp/constants.py index d29413d..4ca5d73 100644 --- a/ovos_plugin_common_play/ocp/constants.py +++ b/ovos_plugin_common_play/ocp/constants.py @@ -1 +1 @@ -from ovos_utils.ocp import OCP_ID +OCP_ID = "ovos.common_play" diff --git a/ovos_plugin_common_play/ocp/gui.py b/ovos_plugin_common_play/ocp/gui.py index 62009c0..d3c1a16 100644 --- a/ovos_plugin_common_play/ocp/gui.py +++ b/ovos_plugin_common_play/ocp/gui.py @@ -1,16 +1,17 @@ import enum from os.path import join, dirname - -from ovos_utils.ocp import OCP_ID, MediaType, PlayerState, LoopState, PlaybackType +from threading import Timer from time import sleep + +from ovos_bus_client.apis.gui import GUIInterface from ovos_bus_client.message import Message from ovos_config import Configuration from ovos_utils.events import EventSchedulerInterface -from ovos_bus_client.apis.gui import GUIInterface from ovos_utils.log import LOG +from ovos_workshop.decorators.ocp import MediaType, PlayerState, LoopState, PlaybackType +from ovos_plugin_common_play.ocp.constants import OCP_ID from ovos_plugin_common_play.ocp.utils import is_qtav_available -from threading import Timer class VideoPlayerBackend(str, enum.Enum): @@ -53,12 +54,12 @@ def bind(self, player): @property def video_backend(self): return self.player.settings.get("video_player_backend") or \ - VideoPlayerBackend.AUTO + VideoPlayerBackend.AUTO @property def home_screen_page(self): return "Home" - + @property def disambiguation_playlists_page(self): return "SuggestionsView" @@ -113,7 +114,7 @@ def update_ocp_skills(self): "title": skill["skill_name"], "image": skill["thumbnail"], "media_type": skill.get("media_type") or [MediaType.GENERIC] - } for skill in self.player.media.get_featured_skills()] + } for skill in self.player.media.get_featured_skills()] self["skillCards"] = skills_cards def update_seekbar_capabilities(self): @@ -142,10 +143,10 @@ def update_current_track(self): self["uri"] = self.player.now_playing.uri self["title"] = self.player.now_playing.title self["image"] = self.player.now_playing.image or \ - join(dirname(__file__), "res/ui/images/ocp.png") + join(dirname(__file__), "res/ui/images/ocp.png") self["artist"] = self.player.now_playing.artist self["bg_image"] = self.player.now_playing.bg_image or \ - join(dirname(__file__), "res/ui/images/ocp_bg.png") + join(dirname(__file__), "res/ui/images/ocp_bg.png") self["duration"] = self.player.now_playing.length self["position"] = self.player.now_playing.position # options below control the web player @@ -154,7 +155,7 @@ def update_current_track(self): # TODO default permissive or restrictive? self["javascript"] = self.player.now_playing.javascript self["javascriptCanOpenWindows"] = False # TODO allow to be defined per track - self["allowUrlChange"] = False # TODO allow to be defined per track + self["allowUrlChange"] = False # TODO allow to be defined per track def update_search_results(self): self["searchModel"] = { @@ -181,30 +182,30 @@ def manage_display(self, page_requested, timeout=None): # This is to ensure that the home is always available to the user # regardless of what other pages are currently open # Swiping from the player to the left will always show the home page - + # The home page will only be in view if the user is not currently playing an active track # If the user is playing a track, the player will be shown instead # This is to ensure that the user always returns to the player when they are playing a track - + # The search_spinner_page has been integrated into the home page as an overlay # It will be shown when the user is searching for a track and will be hidden when the search is complete # on platforms that don't support the notification system - + # Player: # Player loader will always be shown at Protocol level index 1 # The merged playlist and disambiguation pages will always be shown at Protocol level index 2 - + # If the user has just opened the ocp home page, and nothing was played previously, # the player and merged playlist/disambiguation page will not be shown - + # If the user has just opened the ocp home page, and a track was previously played, # the player and merged playlist/disambiguation page will always be shown - + # If the player is not paused or stopped, the player will be shown instead of the home page # when ocp is opened - + # Timeout is used to ensure that ocp is fully closed once the timeout has expired - + sleep(0.2) player_status = self.player.state state2str = {PlayerState.PLAYING: "Playing", PlayerState.PAUSED: "Paused", PlayerState.STOPPED: "Stopped"} @@ -215,11 +216,11 @@ def manage_display(self, page_requested, timeout=None): LOG.debug(f"manage_display: page_requested: {page_requested}") LOG.debug(f"manage_display: player_status: {player_status}") - + if page_requested == "home": self["homepage_index"] = 0 self["displayBottomBar"] = False - + # Check if the skills page has anything to show, only show it if it does if self["skillCards"]: self["displayBottomBar"] = True @@ -238,16 +239,16 @@ def manage_display(self, page_requested, timeout=None): elif page_requested == "playlist": self["displaySuggestionBar"] = False self._show_suggestion_playlist() - + if timeout is not None: self.show_page(self.disambiguation_playlists_page, override_idle=timeout, override_animations=True) else: self.show_page(self.disambiguation_playlists_page, override_idle=True, override_animations=True) - + elif page_requested == "disambiguation": self["displaySuggestionBar"] = False self._show_suggestion_disambiguation() - + if timeout is not None: self.show_page(self.disambiguation_playlists_page, override_idle=timeout, override_animations=True) else: @@ -264,7 +265,7 @@ def cancel_app_view_timeout(self, restart=False): def schedule_app_view_pause_timeout(self): if (self.player.app_view_timeout_enabled - and self.player.app_view_timeout_mode == "pause" + and self.player.app_view_timeout_mode == "pause" and self.player.state == PlayerState.PAUSED): self.schedule_app_view_timeout() @@ -332,19 +333,19 @@ def _get_pages_to_display(self): # determine pages to be shown self["playerBackend"] = self._get_player_page() LOG.debug(f"pages to display backend: {self['playerBackend']}") - + if len(self.player.disambiguation): self["displaySuggestionBar"] = False self._show_suggestion_disambiguation() - + if len(self.player.tracks): self["displaySuggestionBar"] = False self._show_suggestion_playlist() - if len(self.player.disambiguation) and len(self.player.tracks): + if len(self.player.disambiguation) and len(self.player.tracks): self["displaySuggestionBar"] = True self._show_suggestion_playlist() - + pages = [self.player_loader_page, self.disambiguation_playlists_page] return pages @@ -357,7 +358,7 @@ def _show_home_skills(self): def _show_suggestion_playlist(self): self.send_event("ocp.gui.show.suggestion.view.playlist") - + def _show_suggestion_disambiguation(self): self.send_event("ocp.gui.show.suggestion.view.disambiguation") @@ -374,7 +375,7 @@ def handle_play_from_playlist(self, message): def handle_play_from_search(self, message): LOG.debug("Playback requested from search results") - media = message.data["playlistData"] + media = message.data["playlistData"] for track in self.player.disambiguation: if track == media: # found track self.player.play_media(track) @@ -417,7 +418,7 @@ def handle_end_of_playback(self, message=None): # show search results, release screen after 60 seconds if show_results: self.manage_display("playlist", timeout=60) - + def display_notification(self, text, style="info"): """ Display a notification on the screen instead of spinner on platform that support it """ self.show_controlled_notification(text, style=style) @@ -447,7 +448,7 @@ def show_search_spinner(self, persist_home=False): def remove_search_spinner(self): self.send_event("ocp.gui.hide.busy.overlay") - + def remove_homescreen(self): self.release() @@ -467,7 +468,7 @@ def __init__(self, skill_id): # - Extra / Disambiguation / Playlist, this is the page that will be shown when the skill is launched and the skill is playing # - Custom, allow the skill to show any custom page it wants # Page management lifecycle will be handled by the skill itself - + def bind(self, player): self.player = player super().set_bus(self.bus) @@ -476,38 +477,38 @@ def register_screen_type(self, page_url, page_type): for page in self.ocp_registered_pages: if page["type"] == page_type: return - + page_to_register = { "page_url": page_url, "type": page_type } self.ocp_registered_pages[page_type] = page_to_register - + def get_screen_type(self, page_type): return self.ocp_registered_pages[page_type] - + def show_screen(self, page_type, override_idle=False, override_animations=False): page_to_show = self.get_screen_type(page_type) self.show_page(page_to_show["page_url"], override_idle=override_idle, override_animations=override_animations) - + def show_home(self, override_idle=False, override_animations=False): self.show_screen("home", override_idle, override_animations) - + def show_player(self, override_idle=False, override_animations=False): self.show_screen("player", override_idle, override_animations) - + def show_extra(self, override_idle=False, override_animations=False): self.show_screen("extra", override_idle, override_animations) - + def remove_screen(self, page_type): page_to_remove = self.get_screen_type(page_type) self.remove_page(page_to_remove["page_url"]) - + def remove_home(self): self.remove_screen("home") - + def remove_player(self): self.remove_screen("player") - + def remove_extra(self): self.remove_screen("extra") diff --git a/ovos_plugin_common_play/ocp/media.py b/ovos_plugin_common_play/ocp/media.py index 5f56d65..3cad920 100644 --- a/ovos_plugin_common_play/ocp/media.py +++ b/ovos_plugin_common_play/ocp/media.py @@ -1,14 +1,368 @@ from os.path import join, dirname from typing import Union + from ovos_bus_client.client import MessageBusClient from ovos_bus_client.message import Message from ovos_utils.json_helper import merge_dict from ovos_utils.log import LOG -from ovos_utils.ocp import MediaEntry as _ME, Playlist -from ovos_utils.ocp import OCP_ID, MediaState, TrackState, PlaybackType, MediaType +from ovos_workshop.decorators.ocp import MediaState, TrackState, PlaybackType, MediaType +from ovos_plugin_common_play.ocp.constants import OCP_ID from ovos_plugin_common_play.ocp.utils import ocp_plugins +try: + from ovos_utils.ocp import MediaEntry as _ME, Playlist +except ImportError: + # the great uglification + import inspect + import mimetypes + from dataclasses import dataclass + from enum import IntEnum + from typing import Optional, Tuple, List + + + def find_mime(uri): + """ Determine mime type. """ + mime = mimetypes.guess_type(uri) + if mime: + return mime + else: + return None + + + @dataclass + class _ME: + uri: str = "" + title: str = "" + artist: str = "" + match_confidence: int = 0 # 0 - 100 + skill_id: str = OCP_ID + playback: PlaybackType = PlaybackType.UNDEFINED + status: TrackState = TrackState.DISAMBIGUATION + media_type: MediaType = MediaType.GENERIC + length: int = 0 # in seconds + image: str = "" + skill_icon: str = "" + javascript: str = "" # to execute once webview is loaded + + def update(self, entry: dict, skipkeys: list = None, newonly: bool = False): + """ + Update this MediaEntry object with keys from the provided entry + @param entry: dict or MediaEntry object to update this object with + @param skipkeys: list of keys to not change + @param newonly: if True, only adds new keys; existing keys are unchanged + """ + skipkeys = skipkeys or [] + if isinstance(entry, _ME): + entry = entry.as_dict + entry = entry or {} + for k, v in entry.items(): + if k not in skipkeys and hasattr(self, k): + if newonly and self.__getattribute__(k): + # skip, do not replace existing values + continue + self.__setattr__(k, v) + + @property + def infocard(self) -> dict: + """ + Return dict data used for a UI display + """ + return { + "duration": self.length, + "track": self.title, + "image": self.image, + "album": self.skill_id, + "source": self.skill_icon, + "uri": self.uri + } + + @property + def mpris_metadata(self) -> dict: + """ + Return dict data used by MPRIS + """ + from dbus_next.service import Variant + meta = {"xesam:url": Variant('s', self.uri)} + if self.artist: + meta['xesam:artist'] = Variant('as', [self.artist]) + if self.title: + meta['xesam:title'] = Variant('s', self.title) + if self.image: + meta['mpris:artUrl'] = Variant('s', self.image) + if self.length: + meta['mpris:length'] = Variant('d', self.length) + return meta + + @property + def as_dict(self) -> dict: + """ + Return a dict representation of this MediaEntry + """ + return self.__dict__ + + @staticmethod + def from_dict(track: dict): + if track.get("playlist"): + kwargs = {k: v for k, v in track.items() + if k in inspect.signature(Playlist).parameters} + playlist = Playlist(**kwargs) + for e in track["playlist"]: + playlist.add_entry(e) + return playlist + else: + kwargs = {k: v for k, v in track.items() + if k in inspect.signature(MediaEntry).parameters} + return MediaEntry(**kwargs) + + @property + def mimetype(self) -> Optional[Tuple[Optional[str], Optional[str]]]: + """ + Get the detected mimetype tuple (type, encoding) if it can be determined + """ + if self.uri: + return find_mime(self.uri) + + def __eq__(self, other): + if isinstance(other, _ME): + other = other.infocard + # dict comparison + return other == self.infocard + + + @dataclass + class Playlist(list): + title: str = "" + position: int = 0 + length: int = 0 # in seconds + image: str = "" + match_confidence: int = 0 # 0 - 100 + skill_id: str = OCP_ID + skill_icon: str = "" + + def __init__(self, *args, **kwargs): + super().__init__(**kwargs) + list.__init__(self, *args) + + @property + def infocard(self) -> dict: + """ + Return dict data used for a UI display + (model shared with MediaEntry) + """ + return { + "duration": self.length, + "track": self.title, + "image": self.image, + "album": self.skill_id, + "source": self.skill_icon, + "uri": "" + } + + @staticmethod + def from_dict(track: dict): + return MediaEntry.from_dict(track) + + @property + def as_dict(self) -> dict: + """ + Return a dict representation of this MediaEntry + """ + data = { + "title": self.title, + "position": self.position, + "length": self.length, + "image": self.image, + "match_confidence": self.match_confidence, + "skill_id": self.skill_id, + "skill_icon": self.skill_icon, + "playlist": [e.as_dict for e in self.entries] + } + return data + + @property + def entries(self) -> List['MediaEntry']: + """ + Return a list of MediaEntry objects in the playlist + """ + entries = [] + for e in self: + if isinstance(e, dict): + e = MediaEntry.from_dict(e) + if isinstance(e, _ME): + entries.append(e) + return entries + + @property + def current_track(self) -> Optional['MediaEntry']: + """ + Return the current MediaEntry or None if the playlist is empty + """ + if len(self) == 0: + return None + self._validate_position() + track = self[self.position] + if isinstance(track, dict): + track = MediaEntry.from_dict(track) + return track + + @property + def is_first_track(self) -> bool: + """ + Return `True` if the current position is the first track or if the + playlist is empty + """ + if len(self) == 0: + return True + return self.position == 0 + + @property + def is_last_track(self) -> bool: + """ + Return `True` if the current position is the last track of if the + playlist is empty + """ + if len(self) == 0: + return True + return self.position == len(self) - 1 + + def goto_start(self) -> None: + """ + Move to the first entry in the playlist + """ + self.position = 0 + + def clear(self) -> None: + """ + Remove all entries from the Playlist and reset the position + """ + super().clear() + self.position = 0 + + def sort_by_conf(self): + """ + Sort the Playlist by `match_confidence` with high confidence first + """ + self.sort( + key=lambda k: k.match_confidence if isinstance(k, (_ME, Playlist)) + else k.get("match_confidence", 0), reverse=True) + + def add_entry(self, entry: 'MediaEntry', index: int = -1) -> None: + """ + Add an entry at the requested index + @param entry: MediaEntry to add to playlist + @param index: index to insert entry at (default -1 to append) + """ + assert isinstance(index, int) + if index > len(self): + raise ValueError(f"Invalid index {index} requested, " + f"playlist only has {len(self)} entries") + + if isinstance(entry, dict): + entry = MediaEntry.from_dict(entry) + + assert isinstance(entry, (_ME, Playlist)) + + if index == -1: + index = len(self) + + if index < self.position: + self.set_position(self.position + 1) + + self.insert(index, entry) + + def remove_entry(self, entry: Union[int, dict, 'MediaEntry']) -> None: + """ + Remove the requested entry from the playlist or raise a ValueError + @param entry: index or MediaEntry to remove from the playlist + """ + if isinstance(entry, int): + self.pop(entry) + return + if isinstance(entry, dict): + entry = MediaEntry.from_dict(entry) + assert isinstance(entry, _ME) + for idx, e in enumerate(self.entries): + if e == entry: + self.pop(idx) + break + else: + raise ValueError(f"entry not in playlist: {entry}") + + def replace(self, new_list: List[Union[dict, 'MediaEntry']]) -> None: + """ + Replace the contents of this Playlist with new_list + @param new_list: list of MediaEntry or dict objects to set this list to + """ + self.clear() + for e in new_list: + self.add_entry(e) + + def set_position(self, idx: int): + """ + Set the position in the playlist to a specific index + @param idx: Index to set position to + """ + self.position = idx + self._validate_position() + + def goto_track(self, track: Union['MediaEntry', dict]) -> None: + """ + Go to the requested track in the playlist + @param track: MediaEntry to find and go to in the playlist + """ + if isinstance(track, dict): + track = MediaEntry.from_dict(track) + + assert isinstance(track, (_ME, Playlist)) + + if isinstance(track, _ME): + requested_uri = track.uri + else: + requested_uri = track.title + + for idx, t in enumerate(self): + if isinstance(t, _ME): + pl_entry_uri = t.uri + else: + pl_entry_uri = t.title + + if requested_uri == pl_entry_uri: + self.set_position(idx) + LOG.debug(f"New playlist position: {self.position}") + return + LOG.error(f"requested track not in the playlist: {track}") + + def next_track(self) -> None: + """ + Go to the next track in the playlist + """ + self.set_position(self.position + 1) + + def prev_track(self) -> None: + """ + Go to the previous track in the playlist + """ + self.set_position(self.position - 1) + + def _validate_position(self) -> None: + """ + Make sure the current position is valid; default `position` to 0 + """ + if self.position < 0 or self.position >= len(self): + LOG.error(f"Playlist pointer is in an invalid position " + f"({self.position}! Going to start of playlist") + self.position = 0 + + def __contains__(self, item): + if isinstance(item, dict): + item = MediaEntry.from_dict(item) + if isinstance(item, _ME): + for e in self.entries: + if e.uri == item.uri: + return True + return False + class MediaEntry(_ME): def __init__(self, title="", uri="", skill_id=OCP_ID, @@ -141,7 +495,7 @@ def update(self, entry: Union[dict, MediaEntry], skipkeys: list = None, newonly: @param skipkeys: list of keys to not change @param newonly: if True, only adds new keys; existing keys are unchanged """ - if isinstance(entry, MediaEntry): + if isinstance(entry, _ME): entry = entry.as_dict super().update(entry, skipkeys, newonly) # uri updates should not be skipped diff --git a/ovos_plugin_common_play/ocp/mpris.py b/ovos_plugin_common_play/ocp/mpris.py index d3b12fe..e018de2 100644 --- a/ovos_plugin_common_play/ocp/mpris.py +++ b/ovos_plugin_common_play/ocp/mpris.py @@ -9,7 +9,7 @@ from dbus_next.service import ServiceInterface, method, dbus_property, PropertyAccess from ovos_bus_client.message import Message from ovos_utils.log import LOG -from ovos_utils.ocp import TrackState, PlaybackType, PlayerState, LoopState +from ovos_workshop.decorators.ocp import TrackState, PlaybackType, PlayerState, LoopState class MprisPlayerCtl(Thread): diff --git a/ovos_plugin_common_play/ocp/mycroft_cps.py b/ovos_plugin_common_play/ocp/mycroft_cps.py index 3dd1c56..8a26973 100644 --- a/ovos_plugin_common_play/ocp/mycroft_cps.py +++ b/ovos_plugin_common_play/ocp/mycroft_cps.py @@ -4,8 +4,10 @@ from ovos_bus_client.message import Message, dig_for_message from ovos_bus_client.util import wait_for_reply +from ovos_workshop.decorators.ocp import MediaType, PlaybackType + from ovos_plugin_common_play.ocp.base import OCPAbstractComponent -from ovos_utils.ocp import MediaType, PlaybackType, OCP_ID +from ovos_plugin_common_play.ocp.constants import OCP_ID def ensure_uri(s): diff --git a/ovos_plugin_common_play/ocp/player.py b/ovos_plugin_common_play/ocp/player.py index b2bfc7f..49d2104 100644 --- a/ovos_plugin_common_play/ocp/player.py +++ b/ovos_plugin_common_play/ocp/player.py @@ -4,19 +4,19 @@ from typing import List, Union from ovos_bus_client.message import Message - from ovos_config import Configuration -from ovos_plugin_common_play.ocp.gui import OCPMediaPlayerGUI -from ovos_plugin_common_play.ocp.media import NowPlaying -from ovos_plugin_common_play.ocp.mpris import MprisPlayerCtl -from ovos_plugin_common_play.ocp.mycroft_cps import MycroftAudioService -from ovos_plugin_common_play.ocp.search import OCPSearch from ovos_utils.gui import is_gui_connected, is_gui_running from ovos_utils.log import LOG from ovos_utils.messagebus import Message -from ovos_utils.ocp import OCP_ID, Playlist, LoopState, MediaState, PlayerState, TrackState, PlaybackType, PlaybackMode, \ - MediaEntry from ovos_workshop import OVOSAbstractApplication +from ovos_workshop.decorators.ocp import LoopState, MediaState, PlayerState, TrackState, PlaybackType, PlaybackMode + +from ovos_plugin_common_play.ocp.constants import OCP_ID +from ovos_plugin_common_play.ocp.gui import OCPMediaPlayerGUI +from ovos_plugin_common_play.ocp.media import NowPlaying, Playlist, MediaEntry, _ME +from ovos_plugin_common_play.ocp.mpris import MprisPlayerCtl +from ovos_plugin_common_play.ocp.mycroft_cps import MycroftAudioService +from ovos_plugin_common_play.ocp.search import OCPSearch try: from ovos_utils.ocp import dict2entry @@ -246,10 +246,10 @@ def set_now_playing(self, track: Union[dict, MediaEntry]): if isinstance(track, dict): LOG.debug("Handling dict track") track = dict2entry(track) - if not isinstance(track, (MediaEntry, Playlist)): + if not isinstance(track, (_ME, Playlist)): raise ValueError(f"Expected MediaEntry/Playlist, but got: {track}") self.now_playing.reset() # reset now_playing to remove old metadata - if isinstance(track, MediaEntry): + if isinstance(track, _ME): # single track entry (MediaEntry) self.now_playing.update(track) # copy now_playing (without event handlers) to playlist @@ -327,7 +327,7 @@ def play_media(self, track: Union[dict, MediaEntry], """ if isinstance(track, dict): track = dict2entry(track) - if not isinstance(track, (MediaEntry, Playlist)): + if not isinstance(track, (_ME, Playlist)): raise TypeError(f"Expected MediaEntry/Playlist, got: {track}") if isinstance(track, Playlist) and not playlist: playlist = track diff --git a/ovos_plugin_common_play/ocp/search.py b/ovos_plugin_common_play/ocp/search.py index 207ea60..7f5cdc6 100644 --- a/ovos_plugin_common_play/ocp/search.py +++ b/ovos_plugin_common_play/ocp/search.py @@ -4,16 +4,17 @@ from threading import RLock, Lock from typing import List +from ovos_bus_client.message import Message +from ovos_bus_client.util import get_mycroft_bus from ovos_config.locations import get_xdg_config_save_path +from ovos_plugin_manager.ocp import available_extractors from ovos_utils.gui import is_gui_connected, is_gui_running from ovos_utils.log import LOG -from ovos_bus_client.message import Message -from ovos_bus_client.util import get_mycroft_bus +from ovos_workshop.decorators.ocp import MediaType, PlaybackType, PlaybackMode from ovos_plugin_common_play.ocp.base import OCPAbstractComponent +from ovos_plugin_common_play.ocp.constants import OCP_ID from ovos_plugin_common_play.ocp.media import Playlist -from ovos_utils.ocp import OCP_ID, MediaType, TrackState, PlaybackType, PlaybackMode -from ovos_plugin_manager.ocp import available_extractors class OCPQuery: diff --git a/ovos_plugin_common_play/ocp/status.py b/ovos_plugin_common_play/ocp/status.py index 5a6ba4b..ad36991 100644 --- a/ovos_plugin_common_play/ocp/status.py +++ b/ovos_plugin_common_play/ocp/status.py @@ -1 +1,2 @@ -from ovos_utils.ocp import * \ No newline at end of file +from ovos_workshop.decorators.ocp import * +from ovos_plugin_common_play.ocp.constants import OCP_ID \ No newline at end of file diff --git a/ovos_plugin_common_play/ocp/utils.py b/ovos_plugin_common_play/ocp/utils.py index 2800f66..67b8b86 100644 --- a/ovos_plugin_common_play/ocp/utils.py +++ b/ovos_plugin_common_play/ocp/utils.py @@ -1,9 +1,7 @@ import shutil from os import makedirs from os.path import expanduser, isfile, join, dirname, exists -from typing import List -from ovos_ocp_files_plugin.plugin import OCPFilesMetadataExtractor from ovos_plugin_manager.ocp import load_stream_extractors, available_extractors @@ -19,6 +17,7 @@ def is_qtav_available(): def extract_metadata(uri): # backwards compat + from ovos_ocp_files_plugin.plugin import OCPFilesMetadataExtractor return OCPFilesMetadataExtractor.extract_metadata(uri) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 850d727..3545360 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,7 +1,6 @@ ovos-plugin-manager >=0.0.26a5, < 0.1.0 ovos-bus-client>=0.0.7, < 0.1.0 -ovos-utils>=0.1.0a17, < 1.0.0 +ovos-utils>=0.0.38, < 1.0.0 ovos_workshop >=0.0.15, < 0.1.0 -ovos-ocp-files-plugin~=0.13 padacioso~=0.1, >=0.1.1 dbus-next diff --git a/requirements/requirements_extra.txt b/requirements/requirements_extra.txt index 2114711..94a38e0 100644 --- a/requirements/requirements_extra.txt +++ b/requirements/requirements_extra.txt @@ -5,3 +5,4 @@ ovos-ocp-bandcamp-plugin~=0.0, >=0.0.1 ovos-ocp-m3u-plugin>0.0, >=0.0.1 ovos-ocp-news-plugin~=0.0, >=0.0.3 ovos_audio_plugin_simple>=0.0.2a3, < 0.1.0 +ovos-ocp-files-plugin~=0.13 \ No newline at end of file diff --git a/requirements/test.txt b/requirements/test.txt index f4747a2..600fe03 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -5,7 +5,7 @@ pytest-timeout~=2.1 pytest-cov~=4.1 ovos-core~=0.0.7 ovos-audio - +ovos-utils>=0.1.0a17, < 1.0.0 # TODO: Below patching skills_lgpl pyyaml dependency per https://github.com/yaml/pyyaml/issues/724 adapt-parser~=0.5 padacioso~=0.1.2 diff --git a/test/unittests/test_ocp.py b/test/unittests/test_ocp.py index 2902880..4fdb3b3 100644 --- a/test/unittests/test_ocp.py +++ b/test/unittests/test_ocp.py @@ -41,9 +41,6 @@ def _handle_skills_check(msg): self.bus.once('mycroft.skills.is_ready', _handle_skills_check) self.bus.emit(Message('mycroft.ready')) - self.assertTrue(self.ocp._intents_event.is_set()) - - # TODO: Test messagebus event registration def test_ping(self): resp = self.bus.wait_for_response(Message("ovos.common_play.ping"), @@ -78,7 +75,7 @@ def test_classify_media(self): movie = "play a movie" news = "play the latest news" unknown = "play something" - + self.ocp.register_media_intents() self.assertEqual(self.ocp.classify_media(music), MediaType.MUSIC) self.assertEqual(self.ocp.classify_media(movie), MediaType.MOVIE) self.assertEqual(self.ocp.classify_media(news), MediaType.NEWS) diff --git a/test/unittests/test_ocp_media.py b/test/unittests/test_ocp_media.py index 9148aee..7645f77 100644 --- a/test/unittests/test_ocp_media.py +++ b/test/unittests/test_ocp_media.py @@ -3,9 +3,8 @@ from ovos_bus_client import Message -from ovos_plugin_common_play.ocp.media import MediaEntry, NowPlaying +from ovos_plugin_common_play.ocp.media import MediaEntry, NowPlaying, Playlist, _ME as RealMediaEntry from ovos_plugin_common_play.ocp.status import MediaType, PlaybackType, TrackState, MediaState -from ovos_utils.ocp import MediaEntry as RealMediaEntry, Playlist from ovos_utils.messagebus import FakeBus