diff --git a/ovos_plugin_common_play/ocp/__init__.py b/ovos_plugin_common_play/ocp/__init__.py index 04476f1..d5c68b5 100644 --- a/ovos_plugin_common_play/ocp/__init__.py +++ b/ovos_plugin_common_play/ocp/__init__.py @@ -1,3 +1,4 @@ +import time from os.path import join, dirname, isfile from threading import Event, Lock from typing import Optional, List @@ -18,32 +19,6 @@ class OCP(OVOSAbstractApplication): - intent2media = { - "music": MediaType.MUSIC, - "video": MediaType.VIDEO, - "audiobook": MediaType.AUDIOBOOK, - "radio": MediaType.RADIO, - "radio_drama": MediaType.RADIO_THEATRE, - "game": MediaType.GAME, - "tv": MediaType.TV, - "podcast": MediaType.PODCAST, - "news": MediaType.NEWS, - "movie": MediaType.MOVIE, - "short_movie": MediaType.SHORT_FILM, - "silent_movie": MediaType.SILENT_MOVIE, - "bw_movie": MediaType.BLACK_WHITE_MOVIE, - "documentaries": MediaType.DOCUMENTARY, - "comic": MediaType.VISUAL_STORY, - "movietrailer": MediaType.TRAILER, - "behind_scenes": MediaType.BEHIND_THE_SCENES, - - } - # filtered content - adultintents = { - "porn": MediaType.ADULT, - "hentai": MediaType.HENTAI - } - def __init__(self, bus=None, lang=None, settings=None, skill_id=OCP_ID, validate_source: bool = True, native_sources: Optional[List[str]] = None): @@ -54,8 +29,7 @@ def __init__(self, bus=None, lang=None, settings=None, skill_id=OCP_ID, if settings: LOG.debug(f"Updating settings from value passed at init") self.settings.merge(settings) - self._intents_event = Event() - self._intent_registration_lock = Lock() + self.player = OCPMediaPlayer(bus=self.bus, lang=self.lang, settings=self.settings, @@ -64,23 +38,8 @@ def __init__(self, bus=None, lang=None, settings=None, skill_id=OCP_ID, skill_id=OCP_ID, validate_source=validate_source, native_sources=native_sources) - self.media_intents = IntentContainer() self.register_ocp_api_events() - - if self.using_new_pipeline: - LOG.info("Using Classic OCP with experimental OCP pipeline") - else: - self.register_media_intents() - - self.add_event("mycroft.ready", self.replace_mycroft_cps, once=True) - skills_ready = self.bus.wait_for_response( - Message("mycroft.skills.is_ready", - context={"source": [self.skill_id], - "destination": ["skills"]})) - if skills_ready and skills_ready.data.get("status"): - self.remove_event("mycroft.ready") - self.replace_mycroft_cps(skills_ready) - + LOG.info("Using Classic OCP with experimental OCP pipeline") # report available plugins to ovos-core pipeline self.handle_get_SEIs(Message("ovos.common_play.SEI.get")) @@ -98,8 +57,6 @@ def register_ocp_api_events(self): self.add_event('ovos.common_play.SEI.get', self.handle_get_SEIs) self.add_event("ovos.common_play.ping", self.handle_ping) self.add_event('ovos.common_play.home', self.handle_home) - # bus api shared with intents - self.add_event("ovos.common_play.search", self.handle_play) def handle_get_SEIs(self, message): """report available StreamExtractorIds @@ -130,300 +87,6 @@ def handle_home(self, message=None): # homescreen / launch from .desktop self.gui.show_home(app_mode=True) - @property - def using_new_pipeline(self) -> bool: - # 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: - LOG.debug("skipping Classic OCP intent registration") - return - - with self._intent_registration_lock: - if not self._intents_event.is_set(): - LOG.info(f"OCP intents missing, registering for {self}") - self.register_intent("play.intent", self.handle_play) - self.register_intent("read.intent", self.handle_read) - self.register_intent("open.intent", self.handle_open) - self.register_intent("next.intent", self.handle_next) - self.register_intent("prev.intent", self.handle_prev) - self.register_intent("pause.intent", self.handle_pause) - self.register_intent("resume.intent", self.handle_resume) - self._intents_event.set() - - # trigger a presence announcement from all loaded ocp skills - self.bus.emit(Message("ovos.common_play.skills.get")) - - def register_media_intents(self): - """ - NOTE: uses the same format as mycroft .intent files, language - support is handled the same way - """ - locale_folder = join(dirname(__file__), "res", "locale", self.lang) - intents = self.intent2media - if self.settings.get("adult_content", False): - intents.update(self.adultintents) - - for intent_name in intents: - path = join(locale_folder, intent_name + ".intent") - if not isfile(path): - continue - with open(path) as intent: - samples = intent.read().split("\n") - for idx, s in enumerate(samples): - samples[idx] = s.replace("{{", "{").replace("}}", "}") - LOG.debug(f"registering media type intent: {intent_name}") - self.media_intents.add_intent(intent_name, samples) - - def replace_mycroft_cps(self, message=None): - """ - Deactivates any Mycroft playback-control skills and ensures OCP intents - are registered. Registers a listener so this method is called any time - `mycroft.ready` is emitted. - @param message: `mycroft.ready` message triggering this check - """ - mycroft_cps_ids = [ - # disable mycroft cps, ocp replaces it and intents conflict - "skill-playback-control.mycroftai", # the convention - "mycroft-playback-control.mycroftai", # msm install - # (mycroft skills override the repo name ???? ) - "mycroft-playback-control", - "skill-playback-control" # simple git clone - ] - - # disable any loaded mycroft cps skill - for skill_id in mycroft_cps_ids: - self.bus.emit(Message('skillmanager.deactivate', - {"skill": skill_id})) - # register OCP own intents - self.register_ocp_intents() - - # whenever we detect a skill loading, if its mycroft cps disable it! - def unload_mycroft_cps(message): - skill_id = message.data["id"] - if skill_id in mycroft_cps_ids: - self.bus.emit(Message('skillmanager.deactivate', - {"skill": skill_id})) - - if ("mycroft.skills.loaded", unload_mycroft_cps) not in self.events: - self.add_event("mycroft.skills.loaded", unload_mycroft_cps) - - # if skills service (re)loads (re)register OCP - if ("mycroft.ready", self.replace_mycroft_cps) in self.events: - LOG.warning("Method already registered!") - self.add_event("mycroft.ready", self.replace_mycroft_cps, once=True) - def default_shutdown(self): self.player.shutdown() - def classify_media(self, query): - """ this method uses a strict regex based parser to determine what - media type is being requested, this helps in the media process - - only skills that support media type are considered - - if no matches a generic media is performed - - some skills only answer for specific media types, usually to avoid over matching - - skills may use media type to calc confidence - - skills may ignore media type - - NOTE: uses the same format as mycroft .intent files, language - support is handled the same way - """ - if self.voc_match(query, "audio_only"): - query = self.remove_voc(query, "audio_only").strip() - elif self.voc_match(query, "video_only"): - query = self.remove_voc(query, "video_only") - - pred = self.media_intents.calc_intent(query) - LOG.info(f"OVOSCommonPlay MediaType prediction: {pred}") - LOG.debug(f" utterance: {query}") - intent = pred.get("name", "") - if intent in self.intent2media: - return self.intent2media[intent] - LOG.debug("Generic OVOSCommonPlay query") - return MediaType.GENERIC - - # playback control intents - def handle_open(self, message): - """ - Handle open.intent - @param message: Message associated with intent match - """ - self.gui.show_home(app_mode=True) - - def handle_next(self, message): - self.player.play_next() - - def handle_prev(self, message): - self.player.play_prev() - - def handle_pause(self, message): - self.player.pause() - - def handle_stop(self, message=None): - # will stop any playback in GUI and AudioService - try: - return self.player.stop() - except: - pass - - def handle_resume(self, message): - """Resume playback if paused""" - # TODO: Should this also handle "stopped"? - if self.player.state == PlayerState.PAUSED: - self.player.resume() - else: - LOG.info(f"Asked to resume while not paused. state={self.player.state}") - query = self.get_response("play.what") - if query: - message.data["utterance"] = query - self.handle_play(message) - - def handle_play(self, message): - utterance = message.data["utterance"] - phrase = message.data.get("query", "") or utterance - LOG.debug(f"Handle {message.msg_type} request: {phrase}") - num = message.data.get("number", "") - if num: - phrase += " " + num - - # if media is currently paused, empty string means "resume playback" - if self._should_resume(phrase): - self.player.resume() - return - if not phrase: - phrase = self.get_response("play.what") - if not phrase: - # TODO some dialog ? - self.player.stop() - self.gui.show_home(app_mode=True) - return - - # classify the query media type - media_type = self.classify_media(utterance) - - # search common play skills - results = self._search(phrase, utterance, media_type) - self._do_play(phrase, results, media_type) - - # "read XXX" - non "play XXX" audio book intent - def handle_read(self, message): - utterance = message.data["utterance"] - phrase = message.data.get("query", "") or utterance - # search common play skills - results = self._search(phrase, utterance, MediaType.AUDIOBOOK) - self._do_play(phrase, results, MediaType.AUDIOBOOK) - - def _do_play(self, phrase, results, media_type=MediaType.GENERIC): - self.player.reset() - LOG.debug(f"Playing {len(results)} results for: {phrase}") - if not results: - if self.gui: - if self.gui.active_extension == "smartspeaker": - self.gui.display_notification("Sorry, no matches found", style="warning") - - self.speak_dialog("cant.play", - data={"phrase": phrase, - "media_type": media_type}) - - if self.gui: - if "smartspeaker" not in self.gui.active_extension: - if not self.gui.persist_home_display: - self.gui.remove_homescreen() - else: - self.gui.remove_search_spinner() - else: - self.gui.clear_notification() - - else: - if self.gui: - if self.gui.active_extension == "smartspeaker": - self.gui.display_notification("Found a match", style="success") - - best = self.player.media.select_best(results) - self.player.play_media(best, results) - - if self.gui: - if self.gui.active_extension == "smartspeaker": - self.gui.clear_notification() - - self.enclosure.mouth_reset() # TODO display music icon in mk1 - self.set_context("Playing") - - # helper methods - def _search(self, phrase, utterance, media_type): - self.enclosure.mouth_think() - # check if user said "play XXX audio only/no video" - audio_only = False - video_only = False - if self.voc_match(phrase, "audio_only"): - audio_only = True - # dont include "audio only" in search query - phrase = self.remove_voc(phrase, "audio_only") - # dont include "audio only" in media type classification - utterance = self.remove_voc(utterance, "audio_only").strip() - elif self.voc_match(phrase, "video_only"): - video_only = True - # dont include "video only" in search query - phrase = self.remove_voc(phrase, "video_only") - - # Now we place a query on the messsagebus for anyone who wants to - # attempt to service a 'play.request' message. - results = [] - phrase = phrase or utterance - for r in self.player.media.search(phrase, media_type=media_type): - results += r["results"] - LOG.debug(f"Got {len(results)} results") - # ignore very low score matches - results = [r for r in results - if r["match_confidence"] >= self.settings.get("min_score", - 50)] - LOG.debug(f"Got {len(results)} usable results") - - # check if user said "play XXX audio only" - if audio_only: - LOG.info("audio only requested, forcing audio playback " - "unconditionally") - for idx, r in enumerate(results): - # force streams to be played audio only - results[idx]["playback"] = PlaybackType.AUDIO - # check if user said "play XXX video only" - elif video_only: - LOG.info("video only requested, filtering non-video results") - for idx, r in enumerate(results): - if results[idx]["media_type"] == MediaType.VIDEO: - # force streams to be played in video mode, even if - # audio playback requested - results[idx]["playback"] = PlaybackType.VIDEO - # filter audio only streams - results = [r for r in results - if r["playback"] == PlaybackType.VIDEO] - # filter video results if GUI not connected - elif not can_use_gui(self.bus): - LOG.info("unable to use GUI, filtering non-audio results") - # filter video only streams - results = [r for r in results - if r["playback"] in [PlaybackType.AUDIO, PlaybackType.SKILL]] - LOG.debug(f"Returning {len(results)} results") - return results - - def _should_resume(self, phrase: str) -> bool: - """ - Check if a "play" request should resume playback or be handled as a new - session. - @param phrase: Extracted playback phrase - @return: True if player should resume, False if this is a new request - """ - if self.player.state == PlayerState.PAUSED: - if not phrase.strip() or \ - self.voc_match(phrase, "Resume", exact=True) or \ - self.voc_match(phrase, "Play", exact=True): - return True - return False diff --git a/ovos_plugin_common_play/ocp/gui.py b/ovos_plugin_common_play/ocp/gui.py index 40a74cd..1acfac9 100644 --- a/ovos_plugin_common_play/ocp/gui.py +++ b/ovos_plugin_common_play/ocp/gui.py @@ -1,4 +1,5 @@ import enum +import time from os.path import join, dirname from threading import Timer from time import sleep @@ -8,7 +9,8 @@ from ovos_config import Configuration from ovos_utils.events import EventSchedulerInterface from ovos_utils.log import LOG -from ovos_workshop.backwards_compat import MediaType, Playlist, MediaEntry, PlayerState, LoopState, PlaybackType, PluginStream, dict2entry +from ovos_workshop.backwards_compat import (MediaType, Playlist, MediaEntry, PlayerState, LoopState, + PlaybackType, PluginStream, dict2entry) from ovos_plugin_common_play.ocp.constants import OCP_ID from ovos_plugin_common_play.ocp.utils import is_qtav_available @@ -32,7 +34,6 @@ def __init__(self, bus=None): config=gui_config) self.ocp_skills = {} # skill_id: meta self.active_extension = gui_config.get("extension", "generic") - self.notification_timeout = None self.search_mode_is_app = False self.persist_home_display = False self.event_scheduler_interface = None @@ -64,10 +65,6 @@ def home_screen_page(self): def disambiguation_playlists_page(self): return "SuggestionsView" - @property - def audio_player_page(self): - return "OVOSAudioPlayer" - @property def audio_service_page(self): return "OVOSSyncPlayer" @@ -168,13 +165,12 @@ def update_playlist(self): } def show_playback_error(self): - if self.active_extension == "smartspeaker": - self.display_notification("Sorry, An error occurred while playing media") - sleep(0.4) - self.clear_notification() - else: - self["footer_text"] = "Sorry, An error occurred while playing media" - self.remove_search_spinner() + self["title"] = "PLAYBACK ERROR" + # show notification in ovos-shell + self.show_controlled_notification("Sorry, An error occurred while playing media", + style="warning") + time.sleep(2) + self.remove_controlled_notification() def manage_display(self, page_requested, timeout=None): # Home: @@ -187,9 +183,8 @@ def manage_display(self, page_requested, timeout=None): # 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 + # The search_spinner_page will be shown when the user is searching for a track + # and will be hidden when the search is complete # Player: # Player loader will always be shown at Protocol level index 1 @@ -281,7 +276,7 @@ def unload_player_loader(self): def show_home(self, app_mode=True): self.update_ocp_skills() - self.clear_notification() + self.remove_search_spinner() sleep(0.2) self.manage_display("home") @@ -300,10 +295,9 @@ def release(self): super().release() def show_player(self): - # Always clear the spinner and notification before showing the player + # Always clear the spinner before showing the player self.persist_home_display = True self.remove_search_spinner() - self.clear_notification() check_backend = self._get_player_page() if self.get("playerBackend", "") != check_backend: @@ -314,19 +308,11 @@ def show_player(self): # page helpers def _get_player_page(self): - if self.player.active_backend == PlaybackType.AUDIO_SERVICE or \ - self.player.settings.get("force_audioservice", False): - return self.audio_service_page - elif self.player.active_backend == PlaybackType.VIDEO: + if self.player.active_backend == PlaybackType.VIDEO: return self.video_player_page - elif self.player.active_backend == PlaybackType.AUDIO: - return self.audio_player_page elif self.player.active_backend == PlaybackType.WEBVIEW: return self.web_player_page - elif self.player.active_backend == PlaybackType.MPRIS: - return self.audio_service_page - else: # external playback (eg. skill) - # TODO ? + else: return self.audio_service_page def _get_pages_to_display(self): @@ -455,35 +441,12 @@ def handle_end_of_playback(self, message=None): 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) - self.reset_timeout_notification() - - def clear_notification(self): - """ Remove the notification on the screen """ - if self.notification_timeout: - self.notification_timeout.cancel() - self.remove_controlled_notification() - - def start_timeout_notification(self): - """ Remove the notification on the screen after 1 minute of inactivity """ - self.notification_timeout = Timer(60, self.clear_notification).start() - - def reset_timeout_notification(self): - """ Reset the timer to remove the notification """ - if self.notification_timeout: - self.notification_timeout.cancel() - self.start_timeout_notification() - - def show_search_spinner(self, persist_home=False): - self.show_home(app_mode=persist_home) - sleep(0.2) - self.send_event("ocp.gui.show.busy.overlay") - self["footer_text"] = "Querying Skills\n\n" + def notify_search_status(self, text): + self["footer_text"] = text + self.show_page("busy", override_idle=True) def remove_search_spinner(self): - self.send_event("ocp.gui.hide.busy.overlay") + self.remove_page("busy") def remove_homescreen(self): self.release() @@ -531,6 +494,7 @@ 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.remove_search_spinner() self.show_screen("player", override_idle, override_animations) def show_extra(self, override_idle=False, override_animations=False): diff --git a/ovos_plugin_common_play/ocp/mycroft_cps.py b/ovos_plugin_common_play/ocp/mycroft_cps.py deleted file mode 100644 index 6ae76f1..0000000 --- a/ovos_plugin_common_play/ocp/mycroft_cps.py +++ /dev/null @@ -1,314 +0,0 @@ -import time -from datetime import timedelta -from os.path import abspath - -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_plugin_common_play.ocp.constants import OCP_ID - - -def ensure_uri(s): - """Interprete paths as file:// uri's. - - Args: - s: string to be checked - - Returns: - if s is uri, s is returned otherwise file:// is prepended - """ - if isinstance(s, str): - if ':' not in s: - return 'file://' + abspath(s) - else: - return s - elif isinstance(s, (tuple, list)): - if ':' not in s[0]: - return 'file://' + abspath(s[0]), s[1] - else: - return s - else: - raise ValueError('Invalid track') - - -class MycroftAudioService: - """AudioService class for interacting with the mycroft-core audio subsystem - - Args: - bus: Mycroft messagebus connection - """ - - def __init__(self, bus): - self.bus = bus - - @staticmethod - def _format_msg(msg_type, msg_data=None): - # this method ensures all skills are .forward from the utterance - # that triggered the skill, this ensures proper routing and metadata - msg_data = msg_data or {} - msg = dig_for_message() - if msg: - msg = msg.forward(msg_type, msg_data) - else: - msg = Message(msg_type, msg_data) - # at this stage source == skills, lets indicate audio service took over - sauce = msg.context.get("source") - if sauce == "skills": - msg.context["source"] = OCP_ID - return msg - - def queue(self, tracks=None): - """Queue up a track to playing playlist. - - Args: - tracks: track uri or list of track uri's - Each track can be added as a tuple with (uri, mime) - to give a hint of the mime type to the system - """ - tracks = tracks or [] - if isinstance(tracks, (str, tuple)): - tracks = [tracks] - elif not isinstance(tracks, list): - raise ValueError - tracks = [ensure_uri(t) for t in tracks] - msg = self._format_msg('mycroft.audio.service.queue', - {'tracks': tracks}) - self.bus.emit(msg) - - def play(self, tracks=None, utterance=None, repeat=None): - """Start playback. - - Args: - tracks: track uri or list of track uri's - Each track can be added as a tuple with (uri, mime) - to give a hint of the mime type to the system - utterance: forward utterance for further processing by the - audio service. - repeat: if the playback should be looped - """ - repeat = repeat or False - tracks = tracks or [] - utterance = utterance or '' - if isinstance(tracks, (str, tuple)): - tracks = [tracks] - elif not isinstance(tracks, list): - raise ValueError - tracks = [ensure_uri(t) for t in tracks] - msg = self._format_msg('mycroft.audio.service.play', - {'tracks': tracks, - 'utterance': utterance, - 'repeat': repeat}) - self.bus.emit(msg) - - def stop(self): - """Stop the track.""" - msg = self._format_msg('mycroft.audio.service.stop') - self.bus.emit(msg) - - def next(self): - """Change to next track.""" - msg = self._format_msg('mycroft.audio.service.next') - self.bus.emit(msg) - - def prev(self): - """Change to previous track.""" - msg = self._format_msg('mycroft.audio.service.prev') - self.bus.emit(msg) - - def pause(self): - """Pause playback.""" - msg = self._format_msg('mycroft.audio.service.pause') - self.bus.emit(msg) - - def resume(self): - """Resume paused playback.""" - msg = self._format_msg('mycroft.audio.service.resume') - self.bus.emit(msg) - - def get_track_length(self): - """ - getting the duration of the audio in seconds - """ - length = 0 - msg = self._format_msg('mycroft.audio.service.get_track_length') - info = self.bus.wait_for_response(msg, timeout=1) - if info: - length = info.data.get("length", 0) - return length / 1000 # convert to seconds - - def get_track_position(self): - """ - get current position in seconds - """ - pos = 0 - msg = self._format_msg('mycroft.audio.service.get_track_position') - info = self.bus.wait_for_response(msg, timeout=1) - if info: - pos = info.data.get("position", 0) - return pos / 1000 # convert to seconds - - def set_track_position(self, seconds): - """Seek X seconds. - - Arguments: - seconds (int): number of seconds to seek, if negative rewind - """ - msg = self._format_msg('mycroft.audio.service.set_track_position', - {"position": seconds * 1000}) # convert to ms - self.bus.emit(msg) - - def seek(self, seconds=1): - """Seek X seconds. - - Args: - seconds (int): number of seconds to seek, if negative rewind - """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - if seconds < 0: - self.seek_backward(abs(seconds)) - else: - self.seek_forward(seconds) - - def seek_forward(self, seconds=1): - """Skip ahead X seconds. - - Args: - seconds (int): number of seconds to skip - """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - msg = self._format_msg('mycroft.audio.service.seek_forward', - {"seconds": seconds}) - self.bus.emit(msg) - - def seek_backward(self, seconds=1): - """Rewind X seconds - - Args: - seconds (int): number of seconds to rewind - """ - if isinstance(seconds, timedelta): - seconds = seconds.total_seconds() - msg = self._format_msg('mycroft.audio.service.seek_backward', - {"seconds": seconds}) - self.bus.emit(msg) - - def track_info(self): - """Request information of current playing track. - - Returns: - Dict with track info. - """ - msg = self._format_msg('mycroft.audio.service.track_info') - info = self.bus.wait_for_response( - msg, reply_type='mycroft.audio.service.track_info_reply', - timeout=1) - return info.data if info else {} - - def available_backends(self): - """Return available audio backends. - - Returns: - dict with backend names as keys - """ - msg = self._format_msg('mycroft.audio.service.list_backends') - response = self.bus.wait_for_response(msg) - return response.data if response else {} - - @property - def is_playing(self): - """True if the audioservice is playing, else False.""" - return self.track_info() != {} - - -class MycroftCommonPlayInterface(OCPAbstractComponent): - """ interface for mycroft common play """ - - def __init__(self, player=None): - super().__init__(player) - self.query_replies = {} - self.query_extensions = {} - self.waiting = False - self.start_ts = 0 - if player: - self.bind(player) - - def bind(self, player): - self._player = player - self.add_event("play:query.response", - self.handle_cps_response) - - @property - def cps_status(self): - return wait_for_reply('play:status.query', - reply_type="play:status.response", - bus=self.bus).data - - def handle_cps_response(self, message): - search_phrase = message.data["phrase"] - - if ("searching" in message.data and - search_phrase in self.query_extensions): - # Manage requests for time to complete searches - skill_id = message.data["skill_id"] - if message.data["searching"]: - # extend the timeout by N seconds - # IGNORED HERE, used in mycroft-playback-control skill - if skill_id not in self.query_extensions[search_phrase]: - self.query_extensions[search_phrase].append(skill_id) - else: - # Search complete, don't wait on this skill any longer - if skill_id in self.query_extensions[search_phrase]: - self.query_extensions[search_phrase].remove(skill_id) - - elif search_phrase in self.query_replies: - # Collect all replies until the timeout - self.query_replies[message.data["phrase"]].append(message.data) - - # forward response in OCP format - data = self.cps2ocp(message.data) - self.bus.emit(message.forward( - "ovos.common_play.query.response", data)) - - def send_query(self, phrase, media_type=MediaType.GENERIC): - self.query_replies[phrase] = [] - self.query_extensions[phrase] = [] - self.bus.emit(Message('play:query', {"phrase": phrase, - "question_type": media_type})) - - def get_results(self, phrase): - if self.query_replies.get(phrase): - return self.query_replies[phrase] - return [] - - def search(self, phrase, media_type=MediaType.GENERIC, - timeout=5): - self.send_query(phrase, media_type) - self.waiting = True - start_ts = time.time() - while self.waiting and time.time() - start_ts <= timeout: - time.sleep(0.2) - self.waiting = False - return self.get_results(phrase) - - @staticmethod - def cps2ocp(res, media_type=MediaType.GENERIC): - data = { - "playback": PlaybackType.SKILL, - "media_type": media_type, - "is_cps": True, - "cps_data": res['callback_data'], - "skill_id": res["skill_id"], - "phrase": res["phrase"], - 'match_confidence': res["conf"] * 100, - "title": res["phrase"], - "artist": res["skill_id"] - } - return {'phrase': res["phrase"], - "is_old_style": True, - 'results': [data], - 'searching': False, - 'skill_id': res["skill_id"]} diff --git a/ovos_plugin_common_play/ocp/player.py b/ovos_plugin_common_play/ocp/player.py index 696c7ea..4caf1f7 100644 --- a/ovos_plugin_common_play/ocp/player.py +++ b/ovos_plugin_common_play/ocp/player.py @@ -1,11 +1,9 @@ import random from os.path import join, dirname -from time import sleep from typing import List, Union, Optional from ovos_bus_client.message import Message from ovos_config import Configuration -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_workshop import OVOSAbstractApplication @@ -16,7 +14,7 @@ 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_bus_client.apis.ocp import ClassicAudioServiceInterface from ovos_plugin_common_play.ocp.search import OCPSearch from ovos_plugin_common_play.ocp.utils import require_native_source try: @@ -74,7 +72,7 @@ def bind(self, bus=None): self.now_playing.bind(self) self.media.bind(self) self.gui.bind(self) - self.audio_service = MycroftAudioService(self.bus) + self.audio_service = ClassicAudioServiceInterface(self.bus) self.register_bus_handlers() def register_bus_handlers(self): @@ -314,20 +312,19 @@ def validate_stream(self) -> bool: Validate that self.now_playing is playable and update the GUI if it is @return: True if the `now_playing` stream can be handled """ - if self.now_playing.is_cps: - self.now_playing.playback = PlaybackType.SKILL - - if self.active_backend not in [PlaybackType.SKILL, - PlaybackType.UNDEFINED, - PlaybackType.MPRIS]: - has_gui = is_gui_running() or is_gui_connected(self.bus) - if not has_gui or self.settings.get("force_audioservice", False) or \ - self.settings.get("playback_mode") == PlaybackMode.FORCE_AUDIOSERVICE: - # No gui, so lets force playback to use audio only - LOG.debug("Casting to PlaybackType.AUDIO_SERVICE") - self.now_playing.playback = PlaybackType.AUDIO_SERVICE - if not self.now_playing.uri: - return False + if self.active_backend == PlaybackType.AUDIO: + self.now_playing.playback = PlaybackType.AUDIO_SERVICE + + # force playback to use audio only if configured to do so + elif self.active_backend in [PlaybackType.VIDEO] and ( + self.settings.get("force_audioservice", False) or \ + self.settings.get("playback_mode") == PlaybackMode.FORCE_AUDIOSERVICE): + LOG.debug("Casting PlaybackType.VIDEO to PlaybackType.AUDIO_SERVICE") + self.now_playing.playback = PlaybackType.AUDIO_SERVICE + + if not self.now_playing.uri: + return False + self.gui["title"] = f"Extracting info from: {self.now_playing.uri}" # will be reset in self.gui.update_current_track() self.now_playing.extract_stream() self.gui["stream"] = self.now_playing.uri self.gui.update_current_track() @@ -424,13 +421,7 @@ def play(self): self.track_history[self.now_playing.uri] += 1 LOG.debug(f"Requesting playback: {repr(self.active_backend)}") - if self.active_backend == PlaybackType.AUDIO and not is_gui_running(): - # NOTE: this is already normalized in self.validate_stream, using messagebus - # if we get here the GUI probably crashed, or just isnt "mycroft-gui-app" or "ovos-shell" - # is_gui_running() can not be trusted, log a warning only - LOG.warning("Requested Audio playback via GUI, but GUI doesn't seem to be running?") - - if self.active_backend == PlaybackType.AUDIO_SERVICE: + if self.active_backend in [PlaybackType.AUDIO_SERVICE, PlaybackType.AUDIO]: LOG.debug("Handling playback via audio_service") # we explicitly want to use an audio backend for audio only output self.audio_service.play(self.now_playing.uri, @@ -438,29 +429,11 @@ def play(self): self.bus.emit(Message("ovos.common_play.track.state", { "state": TrackState.PLAYING_AUDIOSERVICE})) self.set_player_state(PlayerState.PLAYING) - elif self.active_backend == PlaybackType.AUDIO: - LOG.debug("Handling playback via gui") - # handle audio natively in mycroft-gui - sleep(2) # wait for gui page to start or this is sent before page - self.bus.emit(Message("gui.player.media.service.play", { - "track": self.now_playing.uri, - "mime": self.now_playing.mimetype, - "repeat": False})) - sleep(0.2) # wait for the above message to be processed - self.bus.emit(Message("ovos.common_play.track.state", { - "state": TrackState.PLAYING_AUDIO})) elif self.active_backend == PlaybackType.SKILL: LOG.debug("Requesting playback: PlaybackType.SKILL") - if self.now_playing.is_cps: # mycroft-core compat layer - LOG.debug(" - Mycroft common play result selected") - self.bus.emit(Message('play:start', - {"skill_id": self.now_playing.skill_id, - "callback_data": self.now_playing.cps_data, - "phrase": self.now_playing.phrase})) - else: - self.bus.emit(Message( - f'ovos.common_play.{self.now_playing.skill_id}.play', - self.now_playing.info)) + self.bus.emit(Message( + f'ovos.common_play.{self.now_playing.skill_id}.play', + self.now_playing.info)) self.bus.emit(Message("ovos.common_play.track.state", { "state": TrackState.PLAYING_SKILL})) elif self.active_backend == PlaybackType.VIDEO: diff --git a/ovos_plugin_common_play/ocp/res/ui/+mediacenter/OVOSAudioPlayer.qml b/ovos_plugin_common_play/ocp/res/ui/+mediacenter/OVOSAudioPlayer.qml deleted file mode 100644 index c122d5c..0000000 --- a/ovos_plugin_common_play/ocp/res/ui/+mediacenter/OVOSAudioPlayer.qml +++ /dev/null @@ -1,589 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.12 -import org.kde.kirigami 2.11 as Kirigami -import Mycroft 1.0 as Mycroft -import QtQuick.Layouts 1.12 -import QtGraphicalEffects 1.0 -import QtQuick.Templates 2.12 as T -import QtMultimedia 5.12 -import "../code/helper.js" as HelperJS - -Item { - id: root - - readonly property var audioService: Mycroft.MediaService - - property var source - property string status: "stop" - property var thumbnail: sessionData.image - property var title: sessionData.title - property var author: sessionData.artist - - property var loopStatus: sessionData.loopStatus - property var canResume: sessionData.canResume - property var canNext: sessionData.canNext - property var canPrev: sessionData.canPrev - property var canRepeat: sessionData.canRepeat - property var canShuffle: sessionData.canShuffle - property var shuffleStatus: sessionData.shuffleStatus - - property var playerMeta - property var cpsMeta - - //Player Support Vertical / Horizontal Layouts - property bool horizontalMode: width > height ? 1 : 0 - - //Player Button Control Actions - property var currentState: audioService.playbackState - - //Mediaplayer Related Properties To Be Set By Probe MediaPlayer - property var playerDuration - property var playerPosition - - //Spectrum Related Properties - property var spectrum: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - property var soundModelLength: audioService.spectrum.length - property color spectrumColorNormal: Kirigami.Theme.highlightColor - property color spectrumColorMid: Kirigami.Theme.highlightColor - property color spectrumColorPeak: Kirigami.Theme.textColor - property real spectrumScale: 3 - property bool spectrumVisible: true - readonly property real spectrumHeight: (rep.parent.height / normalize(spectrumScale)) - - onSourceChanged: { - console.log(source) - play() - } - - onFocusChanged: { - if (focus) { - repeatButton.forceActiveFocus() - } - } - - KeyNavigation.down: sliderBar - - Connections { - target: Window.window - onVisibleChanged: { - if(currentState == MediaPlayer.PlayingState) { - stop() - } - } - } - - Timer { - id: sampler - running: true - interval: 100 - repeat: true - onTriggered: { - spectrum = audioService.spectrum - } - } - - function formatedDuration(millis){ - var minutes = Math.floor(millis / 60000); - var seconds = ((millis % 60000) / 1000).toFixed(0); - return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; - } - - function formatedPosition(millis){ - var minutes = Math.floor(millis / 60000); - var seconds = ((millis % 60000) / 1000).toFixed(0); - return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; - } - - function normalize(e){ - switch(e){case.1:return 10;case.2:return 9;case.3:return 8; case.4:return 7;case.5:return 6;case.6:return 5;case.7:return 4;case.8:return 3; case.9:return 2;case 1:return 1; default: return 1} - } - - function play(){ - audioService.playURL(source) - } - - function pause(){ - audioService.playerPause() - } - - function stop(){ - audioService.playerStop() - } - - function resume(){ - audioService.playerContinue() - } - - function next(){ - audioService.playerNext() - } - - function previous(){ - audioService.playerPrevious() - } - - function repeat(){ - audioService.playerRepeat() - } - - function shuffle(){ - audioService.playerShuffle() - } - - function seek(val){ - audioService.playerSeek(val) - } - - function restart(){ - audioService.playerRestart() - } - - Connections { - target: Mycroft.MediaService - - onDurationChanged: { - playerDuration = dur - } - onPositionChanged: { - playerPosition = pos - } - onPlayRequested: { - source = audioService.getTrack() - } - - onStopRequested: { - source = "" - } - - onMediaStatusChanged: { - triggerGuiEvent("media.state", {"state": status}) - if (status == MediaPlayer.EndOfMedia) { - pause() - } - } - - onMetaUpdated: { - root.playerMeta = audioService.getPlayerMeta() - - if(root.playerMeta.hasOwnProperty("Title")) { - root.title = root.playerMeta.Title ? root.playerMeta.Title : "" - } - - if(root.playerMeta.hasOwnProperty("Artist")) { - root.author = root.playerMeta.Artist - } else if(root.playerMeta.hasOwnProperty("ContributingArtist")) { - root.author = root.playerMeta.ContributingArtist - } - console.log("From QML Meta Updated Loading Metainfo") - console.log("Author: " + root.author + " Title: " + root.title) - } - - onMetaReceived: { - root.cpsMeta = audioService.getCPSMeta() - root.thumbnail = root.cpsMeta.thumbnail - root.author = root.cpsMeta.artist - root.title = root.cpsMeta.title - - console.log("From QML Media Received Loading Metainfo") - console.log(JSON.stringify(root.cpsMeta)) - } - } - - Image { - id: imgbackground - anchors.fill: parent - source: root.thumbnail - } - - FastBlur { - anchors.fill: imgbackground - radius: 64 - source: imgbackground - } - - Rectangle { - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.5) - radius: 5 - anchors.fill: parent - anchors.margins: Mycroft.Units.gridUnit * 2 - - GridLayout { - anchors.top: parent.top - anchors.bottom: bottomBoxAboveInner.top - anchors.left: parent.left - anchors.right: parent.right - rows: horizontalMode ? 2 : 1 - columns: horizontalMode ? 2 : 1 - - Rectangle { - id: rct1 - Layout.preferredWidth: horizontalMode ? img.width : parent.width - Layout.preferredHeight: horizontalMode ? parent.height : parent.height * 0.75 - color: "transparent" - - Image { - id: img - property bool rounded: true - property bool adapt: true - source: root.thumbnail - width: parent.height - anchors.horizontalCenter: parent.horizontalCenter - height: width - z: 20 - - layer.enabled: rounded - layer.effect: OpacityMask { - maskSource: Item { - width: img.width - height: img.height - Rectangle { - anchors.centerIn: parent - width: img.adapt ? img.width : Math.min(img.width, img.height) - height: img.adapt ? img.height : width - radius: 5 - } - } - } - } - } - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - color: "transparent" - - ColumnLayout { - id: songTitleText - anchors.fill: parent - anchors.margins: Kirigami.Units.smallSpacing - - Label { - id: authortitle - text: root.author - maximumLineCount: 1 - Layout.fillWidth: true - Layout.fillHeight: true - font.bold: true - font.pixelSize: Math.round(height * 0.45) - fontSizeMode: Text.Fit - minimumPixelSize: Math.round(height * 0.25) - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - font.capitalization: Font.Capitalize - color: Kirigami.Theme.textColor - visible: true - enabled: true - } - - Label { - id: songtitle - text: root.title - maximumLineCount: 1 - Layout.fillWidth: true - Layout.fillHeight: true - font.pixelSize: Math.round(height * 0.45) - fontSizeMode: Text.Fit - minimumPixelSize: Math.round(height * 0.25) - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - font.capitalization: Font.Capitalize - color: Kirigami.Theme.textColor - visible: true - enabled: true - } - } - } - } - - Rectangle { - id: bottomBoxAboveInner - anchors.bottom: sliderBar.top - anchors.left: parent.left - anchors.right: parent.right - height: parent.height * 0.30 - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.5) - - RowLayout { - anchors.top: parent.top - anchors.topMargin: Mycroft.Units.gridUnit - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Mycroft.Units.gridUnit * 2 - anchors.rightMargin: Mycroft.Units.gridUnit * 2 - height: parent.height - z: 2 - - Label { - id: playerPosLabelBottom - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignLeft | Qt.AlignBottom - font.pixelSize: horizontalMode ? height * 0.35 : width * 0.10 - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - text: playerPosition ? formatedPosition(playerPosition) : "" - color: Kirigami.Theme.textColor - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignRight | Qt.AlignBottom - font.pixelSize: horizontalMode ? height * 0.35 : width * 0.10 - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - text: playerDuration ? formatedDuration(playerDuration) : "" - color: Kirigami.Theme.textColor - } - } - - Item { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.bottomMargin: Mycroft.Units.gridUnit * 0.5 - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Kirigami.Units.largeSpacing - anchors.rightMargin: Kirigami.Units.largeSpacing - - Row { - id: visualizationRowItemParent - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: horizontalMode ? parent.width * 0.18 : parent.width * 0.14 - anchors.rightMargin: horizontalMode ? parent.width * 0.18 : parent.width * 0.14 - height: parent.height - spacing: 4 - visible: spectrumVisible - enabled: spectrumVisible - z: -5 - - Repeater { - id: rep - model: root.soundModelLength - 1 - - delegate: Rectangle { - width: (visualizationRowItemParent.width * 0.85) / root.soundModelLength - radius: 3 - opacity: root.currentState === MediaPlayer.PlayingState ? 1 : 0 - height: 15 + root.spectrum[modelData] * root.spectrumHeight - anchors.bottom: parent.bottom - - gradient: Gradient { - GradientStop {position: 0.05; color: height > root.spectrumHeight / 1.25 ? spectrumColorPeak : spectrumColorNormal} - GradientStop {position: 0.25; color: spectrumColorMid} - GradientStop {position: 0.50; color: spectrumColorNormal} - GradientStop {position: 0.85; color: spectrumColorMid} - } - - Behavior on height { - NumberAnimation { - duration: 150 - easing.type: Easing.Linear - } - } - Behavior on opacity { - NumberAnimation{ - duration: 1500 + root.spectrum[modelData] * parent.height - easing.type: Easing.Linear - } - } - } - } - } - } - } - - - T.Slider { - id: sliderBar - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: innerBox.top - height: Mycroft.Units.gridUnit - to: playerDuration - value: playerPosition - z: 10 - - KeyNavigation.up: root - - onPressedChanged: { - root.seek(value) - } - - Keys.onRightPressed: { - root.seek(value + 5000) - } - - Keys.onLeftPressed: { - root.seek(value - 5000) - } - - handle: Item { - x: sliderBar.visualPosition * (parent.width - (Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing)) - anchors.verticalCenter: parent.verticalCenter - height: parent.height + Mycroft.Units.gridUnit - - Rectangle { - id: hand - anchors.verticalCenter: parent.verticalCenter - implicitWidth: Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing - implicitHeight: parent.height - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 1) - border.color: Kirigami.Theme.highlightColor - } - } - - - background: Rectangle { - color: Qt.lighter(Kirigami.Theme.highlightColor, 1.5) - - Rectangle { - width: sliderBar.visualPosition * parent.width - height: parent.height - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0.0; color: Kirigami.Theme.highlightColor } - GradientStop { position: 1.0; color: Qt.darker(Kirigami.Theme.highlightColor, 1.5) } - } - } - } - } - - Rectangle { - id: innerBox - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - height: horizontalMode ? parent.height * 0.25 : parent.height * 0.20 - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.7) - - RowLayout { - id: gridBar - anchors.fill: parent - anchors.margins: Mycroft.Units.gridUnit - spacing: root.horizontalMode ? Mycroft.Units.gridUnit * 0.5 : 1 - z: 10 - - AudioPlayerControl { - id: repeatButton - controlIcon: root.loopStatus === "RepeatTrack" ? Qt.resolvedUrl("../images/media-playlist-repeat-track.svg") : Qt.resolvedUrl("../images/media-playlist-repeat.svg") - controlIconColor: root.loopStatus === "None" ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.3) : Kirigami.Theme.highlightColor - horizontalMode: root.horizontalMode - - KeyNavigation.up: sliderBar - KeyNavigation.right: prevButton - Keys.onReturnPressed: { - clicked() - } - - Keys.onLeftPressed: { - mainLoaderView.movePageLeft() - } - - onClicked: { - repeat() - } - } - - AudioPlayerControl { - id: prevButton - controlIcon: Qt.resolvedUrl("../images/media-skip-backward.svg") - controlIconColor: root.canPrev === true ? Kirigami.Theme.textColor : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.4) - horizontalMode: root.horizontalMode - - KeyNavigation.up: sliderBar - KeyNavigation.left: repeatButton - KeyNavigation.right: playButton - Keys.onReturnPressed: { - clicked() - } - - onClicked: { - previous() - } - } - - AudioPlayerControl { - id: playButton - controlIcon: root.currentState === MediaPlayer.PlayingState ? Qt.resolvedUrl("../images/media-playback-pause.svg") : Qt.resolvedUrl("../images/media-playback-start.svg") - controlIconColor: root.canResume === true ? Kirigami.Theme.textColor : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.4) - horizontalMode: root.horizontalMode - - KeyNavigation.up: sliderBar - KeyNavigation.left: prevButton - KeyNavigation.right: stopButton - Keys.onReturnPressed: { - clicked() - } - - onClicked: { - root.currentState === MediaPlayer.PlayingState ? root.pause() : root.currentState === MediaPlayer.PausedState ? root.resume() : root.play() - } - } - - AudioPlayerControl { - id: stopButton - controlIcon: Qt.resolvedUrl("../images/media-playback-stop.svg") - controlIconColor: Kirigami.Theme.textColor - horizontalMode: root.horizontalMode - - KeyNavigation.up: sliderBar - KeyNavigation.left: playButton - KeyNavigation.right: nextButton - Keys.onReturnPressed: { - clicked() - } - - onClicked: { - if(root.currentState === MediaPlayer.PlayingState) { - root.stop() - } - } - } - - AudioPlayerControl { - id: nextButton - controlIcon: Qt.resolvedUrl("../images/media-skip-forward.svg") - controlIconColor: root.canNext === true ? Kirigami.Theme.textColor : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.4) - horizontalMode: root.horizontalMode - - KeyNavigation.up: sliderBar - KeyNavigation.left: stopButton - KeyNavigation.right: shuffleButton - Keys.onReturnPressed: { - clicked() - } - - onClicked: { - next() - } - } - - AudioPlayerControl { - id: shuffleButton - controlIcon: Qt.resolvedUrl("../images/media-playlist-shuffle.svg") - controlIconColor: root.shuffleStatus === false ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.3) : Kirigami.Theme.highlightColor - horizontalMode: root.horizontalMode - - KeyNavigation.up: sliderBar - KeyNavigation.left: nextButton - Keys.onReturnPressed: { - clicked() - } - - Keys.onRightPressed: { - mainLoaderView.movePageRight() - } - - onClicked: { - shuffle() - } - } - } - } - } -} - diff --git a/ovos_plugin_common_play/ocp/res/ui/+mediacenter/PlayerLoader.qml b/ovos_plugin_common_play/ocp/res/ui/+mediacenter/PlayerLoader.qml index ffc79f0..c42ba1a 100644 --- a/ovos_plugin_common_play/ocp/res/ui/+mediacenter/PlayerLoader.qml +++ b/ovos_plugin_common_play/ocp/res/ui/+mediacenter/PlayerLoader.qml @@ -13,7 +13,7 @@ Mycroft.Delegate { topPadding: 0 bottomPadding: 0 rightPadding: 0 - property var backgroundAllowedPlayers: ["OVOSAudioPlayer.qml", "OVOSSyncPlayer.qml"] + property var backgroundAllowedPlayers: [OVOSSyncPlayer.qml"] function movePageRight(){ parent.parent.parent.currentIndex++ diff --git a/ovos_plugin_common_play/ocp/res/ui/+mediacenter/SpectrumWaveDelegate.qml b/ovos_plugin_common_play/ocp/res/ui/+mediacenter/SpectrumWaveDelegate.qml deleted file mode 100644 index 68a3563..0000000 --- a/ovos_plugin_common_play/ocp/res/ui/+mediacenter/SpectrumWaveDelegate.qml +++ /dev/null @@ -1,256 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.12 -import QtMultimedia 5.12 -import QtGraphicalEffects 1.0 -import QtQuick.Shapes 1.12 -import QtQuick.Templates 2.12 as T -import QtQuick.Controls 2.12 as Controls -import org.kde.kirigami 2.11 as Kirigami -import Mycroft 1.0 as Mycroft - -// Wave animation -Rectangle { - id: rect - color: "transparent" - width: parent.width - height: parent.height - property var position: rect.width - property var spectrumLocalLength - property var spectrumLocalData - - opacity: root.currentState === MediaPlayer.PlayingState ? 1 : 0 - property color spectrumWavePrimaryColor: Kirigami.Theme.highlightColor // Qt.rgba(33/255, 190/255, 166/255, 0.5) - property color spectrumWaveSecondayColor: Kirigami.Theme.textColor // Qt.rgba(33/255, 148/255, 190/255, 0.5) - y: 9 - - function returnRandomFromList() { - var nums = [15, 25, 35] - return nums[Math.floor(Math.random() * nums.length)] - } - - Component.onCompleted: { - console.log("renderType", controlShape.rendererType) - } - - Shape { - id: controlShape - width: parent.width - height: parent.height - anchors.horizontalCenter: parent.horizontalCenter - layer.enabled: true - layer.samples: 16 - rotation: 180 - asynchronous: true - - ShapePath { - objectName: "path1" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width - startY: 0 - - PathQuad { - x: 0; y: 0 - controlX: 0; controlY: 15 + spectrumLocalData[12] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path2" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 2 - startY: 0 - - PathQuad { - x: 0; y: 0 - controlX: 0; controlY: 15 + spectrumLocalData[13] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path3" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 1.5 - startY: 0 - - - PathQuad { - x: 100; y: 0 - controlX: controlShape.width / 3; controlY: 15 + spectrumLocalData[14] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path4" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 2 - 300 - startY: 0 - - PathQuad { - x: 60 * 10; y: 0 - controlX: 440; controlY: randSlider.value - } - } - - ShapePath { - objectName: "path5" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 6 - startY: 0 - - PathQuad { - x: 500; y: 0 - controlX: 400; controlY: 15 + spectrumLocalData[15] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path6" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 8 - startY: 0 - - PathQuad { - x: controlShape.width; y: 0 - controlX: controlShape.width / 1.2 * spectrumLocalData[16]; controlY: 15 + spectrumLocalData[16] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path7" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 2 - startY: 0 - - PathQuad { - x: controlShape.width; y: 0 - controlX: controlShape.width - 10; controlY: 15 + spectrumLocalData[17] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path8" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 100 - startY: 0 - - PathQuad { - x: controlShape.width / 1; y: 0 - controlX: controlShape.width / 2; controlY: 15 + spectrumLocalData[18] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path9" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 200 - startY: 0 - - PathQuad { - x: controlShape.width / 1.5; y: 0 - controlX: controlShape.width / 7; controlY: 15 + spectrumLocalData[11] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path10" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 20 - startY: 0 - - PathQuad { - x: controlShape.width / 1.25; y: 0 - controlX: controlShape.width / 2; controlY: 15 + spectrumLocalData[10] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path11" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - startX: controlShape.width - startY: 0 - - PathQuad { - x: controlShape.width / 1.8; y: 0 - controlX: controlShape.width / 1.2; controlY: 15 + spectrumLocalData[9] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path12" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 2.5 - startY: 0 - - PathQuad { - x: controlShape.width / 1.3; y: 0 - controlX: controlShape.width / 1.6; controlY: 15 + spectrumLocalData[8] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path13" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 20 - startY: 0 - - PathQuad { - x: controlShape.width / 1.9; y: 0 - controlX: controlShape.width / 3; controlY: 15 + spectrumLocalData[19] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path14" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 50 - startY: 0 - - PathQuad { - x: controlShape.width / 3.5; y: 0 - controlX: controlShape.width / 6 * spectrumLocalData[7]; controlY: 15 + spectrumLocalData[7] * (root.spectrumHeight + returnRandomFromList()) - } - } - - Behavior on y { - NumberAnimation { - duration: 150 - easing.type: Easing.Linear - } - } - - Behavior on opacity { - NumberAnimation{ - duration: 1500 * spectrumLocalData[12] + parent.height - easing.type: Easing.Linear - } - } - } -} diff --git a/ovos_plugin_common_play/ocp/res/ui/OVOSAudioPlayer.qml b/ovos_plugin_common_play/ocp/res/ui/OVOSAudioPlayer.qml deleted file mode 100644 index 2cdea07..0000000 --- a/ovos_plugin_common_play/ocp/res/ui/OVOSAudioPlayer.qml +++ /dev/null @@ -1,556 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Controls 2.12 -import org.kde.kirigami 2.11 as Kirigami -import Mycroft 1.0 as Mycroft -import QtQuick.Layouts 1.12 -import QtGraphicalEffects 1.0 -import QtQuick.Templates 2.12 as T -import QtMultimedia 5.12 -import "code/helper.js" as HelperJS - -Item { - id: root - readonly property var audioService: Mycroft.MediaService - - property var source - property string status: "stop" - property var thumbnail: sessionData.image - property var title - property var author - - property var loopStatus: sessionData.loopStatus - property var canResume: sessionData.canResume - property var canNext: sessionData.canNext - property var canPrev: sessionData.canPrev - property var canRepeat: sessionData.canRepeat - property var canShuffle: sessionData.canShuffle - property var shuffleStatus: sessionData.shuffleStatus - - property var playerMeta - property var cpsMeta - - //Player Support Vertical / Horizontal Layouts - property bool horizontalMode: width > height ? 1 : 0 - - //Player Button Control Actions - property var currentState: audioService.playbackState - - //Mediaplayer Related Properties To Be Set By Probe MediaPlayer - property var playerDuration - property var playerPosition - - //Spectrum Related Properties - property var spectrum: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - property var soundModelLength: audioService.spectrum.length - property color spectrumColorNormal: Kirigami.Theme.highlightColor - property color spectrumColorMid: Kirigami.Theme.highlightColor - property color spectrumColorPeak: Kirigami.Theme.textColor - property real spectrumScale: 2.75 - property bool spectrumVisible: true - property int spectrumType: sessionData.visualizationType ? sessionData.visualizationType : 1 // 1. Bars, 2. Waves - readonly property real spectrumHeight: (rep.parent.height / normalize(spectrumScale)) - - onSourceChanged: { - console.log(source) - play() - } - - Timer { - id: sampler - running: true - interval: 100 - repeat: true - onTriggered: { - spectrum = audioService.spectrum - } - } - - function formatedDuration(millis){ - var minutes = Math.floor(millis / 60000); - var seconds = ((millis % 60000) / 1000).toFixed(0); - return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; - } - - function formatedPosition(millis){ - var minutes = Math.floor(millis / 60000); - var seconds = ((millis % 60000) / 1000).toFixed(0); - return minutes + ":" + (seconds < 10 ? '0' : '') + seconds; - } - - function normalize(e){ - switch(e){case.1:return 10;case.2:return 9;case.3:return 8; case.4:return 7;case.5:return 6;case.6:return 5;case.7:return 4;case.8:return 3; case.9:return 2;case 1:return 1; default: return 1} - } - - function play(){ - audioService.playURL(source) - } - - function pause(){ - audioService.playerPause() - } - - function stop(){ - audioService.playerStop() - } - - function resume(){ - audioService.playerContinue() - } - - function next(){ - audioService.playerNext() - } - - function previous(){ - audioService.playerPrevious() - } - - function repeat(){ - audioService.playerRepeat() - } - - function shuffle(){ - audioService.playerShuffle() - } - - function seek(val){ - audioService.playerSeek(val) - } - - function restart(){ - audioService.playerRestart() - } - - Connections { - target: Mycroft.MediaService - - onDurationChanged: { - playerDuration = dur - } - onPositionChanged: { - playerPosition = pos - } - onPlayRequested: { - source = audioService.getTrack() - } - - onStopRequested: { - source = "" - root.title = "" - root.author = "" - } - - onMediaStatusChanged: { - triggerGuiEvent("media.state", {"state": status}) - if (status == MediaPlayer.EndOfMedia) { - pause() - } - } - - onMetaUpdated: { - root.playerMeta = audioService.getPlayerMeta() - - if(root.playerMeta.hasOwnProperty("Title")) { - root.title = root.playerMeta.Title ? root.playerMeta.Title : "" - } - - if(root.playerMeta.hasOwnProperty("Artist")) { - root.author = root.playerMeta.Artist - } else if(root.playerMeta.hasOwnProperty("ContributingArtist")) { - root.author = root.playerMeta.ContributingArtist - } - console.log("From QML Meta Updated Loading Metainfo") - console.log("Author: " + root.author + " Title: " + root.title) - } - - onMetaReceived: { - root.cpsMeta = audioService.getCPSMeta() - root.thumbnail = root.cpsMeta.thumbnail - root.author = root.cpsMeta.artist - root.title = root.cpsMeta.title ? root.cpsMeta.title : "" - - console.log("From QML Media Received Loading Metainfo") - console.log(JSON.stringify(root.cpsMeta)) - } - } - - Image { - id: imgbackground - anchors.fill: parent - source: root.thumbnail - - MouseArea { - anchors.fill: parent - onClicked: { - genericCloseControl.show() - } - } - } - - FastBlur { - anchors.fill: imgbackground - radius: 64 - source: imgbackground - } - - Rectangle { - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.5) - radius: 5 - anchors.fill: parent - anchors.margins: Mycroft.Units.gridUnit * 2 - - GridLayout { - anchors.top: parent.top - anchors.bottom: bottomBoxAboveInner.top - anchors.left: parent.left - anchors.right: parent.right - rows: horizontalMode ? 2 : 1 - columns: horizontalMode ? 2 : 1 - - Rectangle { - id: rct1 - Layout.preferredWidth: horizontalMode ? img.width : parent.width - Layout.preferredHeight: horizontalMode ? parent.height : parent.height * 0.75 - color: "transparent" - - Image { - id: img - property bool rounded: true - property bool adapt: true - source: root.thumbnail - width: parent.height - anchors.horizontalCenter: parent.horizontalCenter - height: width - z: 20 - - layer.enabled: rounded - layer.effect: OpacityMask { - maskSource: Item { - width: img.width - height: img.height - Rectangle { - anchors.centerIn: parent - width: img.adapt ? img.width : Math.min(img.width, img.height) - height: img.adapt ? img.height : width - radius: 5 - } - } - } - } - } - Rectangle { - Layout.fillWidth: true - Layout.fillHeight: true - color: "transparent" - - ColumnLayout { - id: songTitleText - anchors.fill: parent - anchors.margins: Kirigami.Units.smallSpacing - - Label { - id: authortitle - text: root.author - maximumLineCount: 1 - Layout.fillWidth: true - Layout.fillHeight: true - font.bold: true - font.pixelSize: Math.round(height * 0.45) - fontSizeMode: Text.Fit - minimumPixelSize: Math.round(height * 0.25) - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - font.capitalization: Font.Capitalize - color: Kirigami.Theme.textColor - visible: true - enabled: true - } - - Label { - id: songtitle - text: root.title - maximumLineCount: 1 - Layout.fillWidth: true - Layout.fillHeight: true - font.pixelSize: Math.round(height * 0.45) - fontSizeMode: Text.Fit - minimumPixelSize: Math.round(height * 0.25) - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - font.capitalization: Font.Capitalize - color: Kirigami.Theme.textColor - visible: true - enabled: true - } - } - } - } - - Rectangle { - id: bottomBoxAboveInner - anchors.bottom: sliderBar.top - anchors.left: parent.left - anchors.right: parent.right - height: parent.height * 0.30 - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.5) - - RowLayout { - anchors.top: parent.top - anchors.topMargin: Mycroft.Units.gridUnit - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Mycroft.Units.gridUnit * 2 - anchors.rightMargin: Mycroft.Units.gridUnit * 2 - height: parent.height - z: 2 - - Label { - id: playerPosLabelBottom - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignLeft | Qt.AlignBottom - font.pixelSize: horizontalMode ? height * 0.35 : width * 0.10 - horizontalAlignment: Text.AlignLeft - verticalAlignment: Text.AlignVCenter - text: playerPosition ? formatedPosition(playerPosition) : "" - color: Kirigami.Theme.textColor - } - - Label { - Layout.fillWidth: true - Layout.fillHeight: true - Layout.alignment: Qt.AlignRight | Qt.AlignBottom - font.pixelSize: horizontalMode ? height * 0.35 : width * 0.10 - horizontalAlignment: Text.AlignRight - verticalAlignment: Text.AlignVCenter - text: playerDuration ? formatedDuration(playerDuration) : "" - color: Kirigami.Theme.textColor - } - } - - Item { - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.bottomMargin: Mycroft.Units.gridUnit * 0.5 - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: Kirigami.Units.largeSpacing - anchors.rightMargin: Kirigami.Units.largeSpacing - - MouseArea { - anchors.fill: parent - onClicked: { - if(root.spectrumType == 1) { - root.spectrumType = 2 - Mycroft.MycroftController.sendRequest("ovos.common_play.spectrum", {"type": 2}) - } else { - root.spectrumType = 1 - Mycroft.MycroftController.sendRequest("ovos.common_play.spectrum", {"type": 1}) - } - } - } - - Row { - id: visualizationRowItemParent - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: horizontalMode ? parent.width * 0.18 : parent.width * 0.14 - anchors.rightMargin: horizontalMode ? parent.width * 0.18 : parent.width * 0.14 - height: parent.height - spacing: 4 - visible: spectrumVisible && spectrumType == 1 - enabled: spectrumVisible && spectrumType == 1 - - Repeater { - id: rep - model: root.soundModelLength - 1 - - delegate: Rectangle { - width: (visualizationRowItemParent.width * 0.85) / root.soundModelLength - radius: 3 - opacity: root.currentState === MediaPlayer.PlayingState ? 1 : 0 - height: 15 + root.spectrum[modelData] * root.spectrumHeight - anchors.bottom: parent.bottom - - gradient: Gradient { - GradientStop {position: 0.05; color: height > root.spectrumHeight / 1.25 ? spectrumColorPeak : spectrumColorNormal} - GradientStop {position: 0.25; color: spectrumColorMid} - GradientStop {position: 0.50; color: spectrumColorNormal} - GradientStop {position: 0.85; color: spectrumColorMid} - } - - Behavior on height { - NumberAnimation { - duration: 150 - easing.type: Easing.Linear - } - } - Behavior on opacity { - NumberAnimation{ - duration: 1500 + root.spectrum[modelData] * parent.height - easing.type: Easing.Linear - } - } - } - } - } - - SpectrumWaveDelegate { - height: parent.height - anchors.horizontalCenter: parent.horizontalCenter - visible: spectrumVisible && spectrumType == 2 - enabled: spectrumVisible && spectrumType == 2 - spectrumLocalLength: root.soundModelLength - spectrumLocalData: root.spectrum - } - } - } - - - T.Slider { - id: sliderBar - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: innerBox.top - height: Mycroft.Units.gridUnit - to: playerDuration - value: playerPosition - z: 10 - - onPressedChanged: { - root.seek(value) - - if(pressed) { - hand.color = Kirigami.Theme.highlightColor - hand.border.color = Kirigami.Theme.backgroundColor - } else { - hand.color = Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 1) - hand.border.color = Kirigami.Theme.highlightColor - } - } - - handle: Item { - x: sliderBar.visualPosition * (parent.width - (Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing)) - anchors.verticalCenter: parent.verticalCenter - height: parent.height + Mycroft.Units.gridUnit - - Rectangle { - id: hand - anchors.verticalCenter: parent.verticalCenter - implicitWidth: Kirigami.Units.iconSizes.small + Kirigami.Units.smallSpacing - implicitHeight: parent.height - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 1) - border.color: Kirigami.Theme.highlightColor - } - } - - - background: Rectangle { - color: Qt.lighter(Kirigami.Theme.highlightColor, 1.5) - - Rectangle { - width: sliderBar.visualPosition * parent.width - height: parent.height - gradient: Gradient { - orientation: Gradient.Horizontal - GradientStop { position: 0.0; color: Kirigami.Theme.highlightColor } - GradientStop { position: 1.0; color: Qt.darker(Kirigami.Theme.highlightColor, 1.5) } - } - } - } - } - - Rectangle { - id: innerBox - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right - height: root.horizontalMode ? Math.max(Mycroft.Units.gridUnit * 6, parent.height * 0.25) : Math.max(Mycroft.Units.gridUnit * 5, parent.height * 0.20) - - color: Qt.rgba(Kirigami.Theme.backgroundColor.r, Kirigami.Theme.backgroundColor.g, Kirigami.Theme.backgroundColor.b, 0.7) - - RowLayout { - id: gridBar - anchors.fill: parent - anchors.margins: Mycroft.Units.gridUnit - spacing: root.horizontalMode ? Mycroft.Units.gridUnit * 0.5 : 1 - z: 10 - - AudioPlayerControl { - id: repeatButton - controlIcon: root.loopStatus === "RepeatTrack" ? Qt.resolvedUrl("images/media-playlist-repeat-track.svg") : Qt.resolvedUrl("images/media-playlist-repeat.svg") - controlIconColor: root.loopStatus === "None" ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.3) : Kirigami.Theme.highlightColor - horizontalMode: root.horizontalMode - - onClicked: { - repeat() - } - } - - AudioPlayerControl { - id: prevButton - controlIcon: Qt.resolvedUrl("images/media-skip-backward.svg") - controlIconColor: root.canPrev === true ? Kirigami.Theme.textColor : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.4) - horizontalMode: root.horizontalMode - - onClicked: { - previous() - } - } - - AudioPlayerControl { - id: playButton - controlIcon: root.currentState === MediaPlayer.PlayingState ? Qt.resolvedUrl("images/media-playback-pause.svg") : Qt.resolvedUrl("images/media-playback-start.svg") - controlIconColor: root.canResume === true ? Kirigami.Theme.textColor : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.4) - horizontalMode: root.horizontalMode - - onClicked: { - root.currentState === MediaPlayer.PlayingState ? root.pause() : root.currentState === MediaPlayer.PausedState ? root.resume() : root.play() - } - } - - AudioPlayerControl { - id: stopButton - controlIcon: Qt.resolvedUrl("images/media-playback-stop.svg") - controlIconColor: Kirigami.Theme.textColor - horizontalMode: root.horizontalMode - - onClicked: { - if(root.currentState === MediaPlayer.PlayingState) { - root.stop() - } - } - } - - AudioPlayerControl { - id: nextButton - controlIcon: Qt.resolvedUrl("images/media-skip-forward.svg") - controlIconColor: root.canNext === true ? Kirigami.Theme.textColor : Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.4) - horizontalMode: root.horizontalMode - - onClicked: { - next() - } - } - - AudioPlayerControl { - id: shuffleButton - controlIcon: Qt.resolvedUrl("images/media-playlist-shuffle.svg") - controlIconColor: root.shuffleStatus === false ? Qt.rgba(Kirigami.Theme.textColor.r, Kirigami.Theme.textColor.g, Kirigami.Theme.textColor.b, 0.3) : Kirigami.Theme.highlightColor - horizontalMode: root.horizontalMode - - onClicked: { - shuffle() - } - } - } - } - } - - GenericCloseControl { - id: genericCloseControl - } -} diff --git a/ovos_plugin_common_play/ocp/res/ui/PlayerLoader.qml b/ovos_plugin_common_play/ocp/res/ui/PlayerLoader.qml index 11dabf4..41ab8b4 100644 --- a/ovos_plugin_common_play/ocp/res/ui/PlayerLoader.qml +++ b/ovos_plugin_common_play/ocp/res/ui/PlayerLoader.qml @@ -13,7 +13,7 @@ Mycroft.Delegate { topPadding: 0 bottomPadding: 0 rightPadding: 0 - property var backgroundAllowedPlayers: ["OVOSAudioPlayer.qml", "OVOSSyncPlayer.qml"] + property var backgroundAllowedPlayers: ["OVOSSyncPlayer.qml"] onGuiEvent: { switch (eventName) { diff --git a/ovos_plugin_common_play/ocp/res/ui/SpectrumWaveDelegate.qml b/ovos_plugin_common_play/ocp/res/ui/SpectrumWaveDelegate.qml deleted file mode 100644 index 68a3563..0000000 --- a/ovos_plugin_common_play/ocp/res/ui/SpectrumWaveDelegate.qml +++ /dev/null @@ -1,256 +0,0 @@ -import QtQuick 2.12 -import QtQuick.Window 2.12 -import QtQuick.Layouts 1.12 -import QtMultimedia 5.12 -import QtGraphicalEffects 1.0 -import QtQuick.Shapes 1.12 -import QtQuick.Templates 2.12 as T -import QtQuick.Controls 2.12 as Controls -import org.kde.kirigami 2.11 as Kirigami -import Mycroft 1.0 as Mycroft - -// Wave animation -Rectangle { - id: rect - color: "transparent" - width: parent.width - height: parent.height - property var position: rect.width - property var spectrumLocalLength - property var spectrumLocalData - - opacity: root.currentState === MediaPlayer.PlayingState ? 1 : 0 - property color spectrumWavePrimaryColor: Kirigami.Theme.highlightColor // Qt.rgba(33/255, 190/255, 166/255, 0.5) - property color spectrumWaveSecondayColor: Kirigami.Theme.textColor // Qt.rgba(33/255, 148/255, 190/255, 0.5) - y: 9 - - function returnRandomFromList() { - var nums = [15, 25, 35] - return nums[Math.floor(Math.random() * nums.length)] - } - - Component.onCompleted: { - console.log("renderType", controlShape.rendererType) - } - - Shape { - id: controlShape - width: parent.width - height: parent.height - anchors.horizontalCenter: parent.horizontalCenter - layer.enabled: true - layer.samples: 16 - rotation: 180 - asynchronous: true - - ShapePath { - objectName: "path1" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width - startY: 0 - - PathQuad { - x: 0; y: 0 - controlX: 0; controlY: 15 + spectrumLocalData[12] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path2" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 2 - startY: 0 - - PathQuad { - x: 0; y: 0 - controlX: 0; controlY: 15 + spectrumLocalData[13] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path3" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 1.5 - startY: 0 - - - PathQuad { - x: 100; y: 0 - controlX: controlShape.width / 3; controlY: 15 + spectrumLocalData[14] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path4" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 2 - 300 - startY: 0 - - PathQuad { - x: 60 * 10; y: 0 - controlX: 440; controlY: randSlider.value - } - } - - ShapePath { - objectName: "path5" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 6 - startY: 0 - - PathQuad { - x: 500; y: 0 - controlX: 400; controlY: 15 + spectrumLocalData[15] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path6" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 8 - startY: 0 - - PathQuad { - x: controlShape.width; y: 0 - controlX: controlShape.width / 1.2 * spectrumLocalData[16]; controlY: 15 + spectrumLocalData[16] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path7" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 2 - startY: 0 - - PathQuad { - x: controlShape.width; y: 0 - controlX: controlShape.width - 10; controlY: 15 + spectrumLocalData[17] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path8" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 100 - startY: 0 - - PathQuad { - x: controlShape.width / 1; y: 0 - controlX: controlShape.width / 2; controlY: 15 + spectrumLocalData[18] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path9" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 200 - startY: 0 - - PathQuad { - x: controlShape.width / 1.5; y: 0 - controlX: controlShape.width / 7; controlY: 15 + spectrumLocalData[11] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path10" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 20 - startY: 0 - - PathQuad { - x: controlShape.width / 1.25; y: 0 - controlX: controlShape.width / 2; controlY: 15 + spectrumLocalData[10] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path11" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - startX: controlShape.width - startY: 0 - - PathQuad { - x: controlShape.width / 1.8; y: 0 - controlX: controlShape.width / 1.2; controlY: 15 + spectrumLocalData[9] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path12" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 2.5 - startY: 0 - - PathQuad { - x: controlShape.width / 1.3; y: 0 - controlX: controlShape.width / 1.6; controlY: 15 + spectrumLocalData[8] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path13" - fillColor: spectrumWavePrimaryColor - strokeColor: spectrumWavePrimaryColor - strokeWidth: 0 - startX: controlShape.width / 20 - startY: 0 - - PathQuad { - x: controlShape.width / 1.9; y: 0 - controlX: controlShape.width / 3; controlY: 15 + spectrumLocalData[19] * (root.spectrumHeight + returnRandomFromList()) - } - } - - ShapePath { - objectName: "path14" - fillColor: spectrumWaveSecondayColor - strokeColor: spectrumWaveSecondayColor - strokeWidth: 0 - startX: controlShape.width / 50 - startY: 0 - - PathQuad { - x: controlShape.width / 3.5; y: 0 - controlX: controlShape.width / 6 * spectrumLocalData[7]; controlY: 15 + spectrumLocalData[7] * (root.spectrumHeight + returnRandomFromList()) - } - } - - Behavior on y { - NumberAnimation { - duration: 150 - easing.type: Easing.Linear - } - } - - Behavior on opacity { - NumberAnimation{ - duration: 1500 * spectrumLocalData[12] + parent.height - easing.type: Easing.Linear - } - } - } -} diff --git a/ovos_plugin_common_play/ocp/res/ui/animations/installing.json b/ovos_plugin_common_play/ocp/res/ui/animations/installing.json new file mode 100644 index 0000000..ab84a1b --- /dev/null +++ b/ovos_plugin_common_play/ocp/res/ui/animations/installing.json @@ -0,0 +1 @@ +{"v":"5.4.3","fr":29.9700012207031,"ip":0,"op":70.0000028511585,"w":307,"h":389,"nm":"refresh-button","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-44,"ix":10},"p":{"a":0,"k":[154.149,195.327,0],"ix":2},"a":{"a":0,"k":[-2.021,-4,0],"ix":1},"s":{"a":0,"k":[71.946,71.946,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[217,217],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.211764705882,0.211764705882,0.211764705882,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":33,"ix":1}},{"n":"o","nm":"offset","v":{"a":0,"k":0,"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-1.5,-4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.625],"y":[0]},"n":["0p667_1_0p625_0"],"t":26,"s":[0],"e":[100]},{"t":65.0000026475043}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"n":["0p667_1_1_0"],"t":7,"s":[0],"e":[100]},{"t":41.0000016699642}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[-179],"e":[181]},{"t":65.0000026475043}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-44,"ix":10},"p":{"a":0,"k":[154.149,195.327,0],"ix":2},"a":{"a":0,"k":[-2.021,-4,0],"ix":1},"s":{"a":0,"k":[71.946,71.946,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[217,217],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.9098039215686274,0.3137254901960784,0.3137254901960784,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":33,"ix":1}},{"n":"o","nm":"offset","v":{"a":0,"k":0,"ix":7}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-1.5,-4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.625],"y":[0]},"n":["0p667_1_0p625_0"],"t":26,"s":[0],"e":[100]},{"t":65.0000026475043}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"n":["0p667_1_1_0"],"t":0,"s":[0],"e":[100]},{"t":41.0000016699642}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[-224],"e":[136]},{"t":65.0000026475043}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-44,"ix":10},"p":{"a":0,"k":[154.149,195.327,0],"ix":2},"a":{"a":0,"k":[-2.021,-4,0],"ix":1},"s":{"a":0,"k":[46.072,46.072,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[217,217],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.9058823529411765,0.2627450980392157,0.2627450980392157,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-1.5,-4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"n":["0p667_1_1_0"],"t":0,"s":[100],"e":[0]},{"t":38.0000015477717}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.733],"y":[0.015]},"n":["0p667_1_0p733_0p015"],"t":19,"s":[100],"e":[0]},{"t":60.0000024438501}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[-319],"e":[-679]},{"t":65.0000026475043}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-44,"ix":10},"p":{"a":0,"k":[154.149,195.327,0],"ix":2},"a":{"a":0,"k":[-2.021,-4,0],"ix":1},"s":{"a":0,"k":[46.072,46.072,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[217,217],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.105882352941,0.105882352941,0.105882352941,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-1.5,-4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"n":["0p667_1_1_0"],"t":0,"s":[100],"e":[0]},{"t":38.0000015477717}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.733],"y":[0.015]},"n":["0p667_1_0p733_0p015"],"t":19,"s":[100],"e":[0]},{"t":60.0000024438501}],"ix":2},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[-224],"e":[-584]},{"t":65.0000026475043}],"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[4.436],"e":[-355.564]},{"t":69.0000028104276}],"ix":10},"p":{"a":0,"k":[155,194,0],"ix":2},"a":{"a":0,"k":[-1.5,-4,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[217,217],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.47843137254901963,0.47843137254901963,0.47843137254901963,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-1.5,-4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":85,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":0,"s":[4.436],"e":[-355.564]},{"t":69.0000028104276}],"ix":10},"p":{"a":0,"k":[155,194,0],"ix":2},"a":{"a":0,"k":[-1.5,-4,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[217,217],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.3607843137254902,0.16470588235294117,0.16470588235294117,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":4,"ml2":{"a":0,"k":4,"ix":8},"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[-1.5,-4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":0,"k":0,"ix":1},"e":{"a":0,"k":85,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"refresh-button Outlines","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":85.093,"ix":10},"p":{"a":0,"k":[-6.619,-112.041,0],"ix":2},"a":{"a":0,"k":[188.881,115.959,0],"ix":1},"s":{"a":0,"k":[58.516,58.516,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[1.875,-1.875],[-1.875,-1.875],[0,0],[-1.274,0],[-0.898,0.903],[0,0],[1.875,1.875],[1.875,-1.875],[0,0]],"o":[[-1.875,-1.875],[-1.875,1.875],[0,0],[0.899,0.903],[1.277,0],[0,0],[1.875,-1.875],[-1.875,-1.875],[0,0],[0,0]],"v":[[-14.662,-12.188],[-21.451,-12.188],[-21.451,-5.398],[-3.393,12.656],[-0.002,14.063],[3.392,12.656],[21.451,-5.398],[21.451,-12.188],[14.662,-12.188],[-0.002,2.473]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[-14.662,-12.188]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.9058823529411765,0.2627450980392157,0.2627450980392157,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[188.943,118.162],"ix":2},"a":{"a":0,"k":[0.062,2.438],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":70.0000028511585,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/ovos_plugin_common_play/ocp/res/ui/busy.qml b/ovos_plugin_common_play/ocp/res/ui/busy.qml new file mode 100644 index 0000000..2a58cd5 --- /dev/null +++ b/ovos_plugin_common_play/ocp/res/ui/busy.qml @@ -0,0 +1,49 @@ +import QtQuick.Layouts 1.4 +import QtQuick 2.4 +import QtQuick.Controls 2.0 +import org.kde.kirigami 2.4 as Kirigami + +import Mycroft 1.0 as Mycroft +import org.kde.lottie 1.0 +import QtGraphicalEffects 1.0 + +Mycroft.Delegate { + id: loadingScreen + fillWidth: true + + Rectangle { + anchors.fill: parent + color: Kirigami.Theme.backgroundColor + + ColumnLayout { + id: grid + anchors.fill: parent + anchors.margins: Kirigami.Units.largeSpacing + + Label { + id: statusLabel + Layout.alignment: Qt.AlignHCenter + font.pixelSize: parent.height * 0.075 + wrapMode: Text.WordWrap + renderType: Text.NativeRendering + font.family: "Noto Sans Display" + font.styleName: "Black" + text: "Searching Media" + color: Kirigami.Theme.textColor + } + + LottieAnimation { + id: statusIcon + visible: true + enabled: true + Layout.fillWidth: true + Layout.fillHeight: true + Layout.alignment: Qt.AlignHCenter + loops: Animation.Infinite + fillMode: Image.PreserveAspectFit + running: true + source: Qt.resolvedUrl("animations/installing.json") + } + } + } +} \ No newline at end of file diff --git a/ovos_plugin_common_play/ocp/res/ui/qmldir b/ovos_plugin_common_play/ocp/res/ui/qmldir index 8acdc98..d7498f5 100644 --- a/ovos_plugin_common_play/ocp/res/ui/qmldir +++ b/ovos_plugin_common_play/ocp/res/ui/qmldir @@ -8,7 +8,6 @@ GenericCloseControl 1.0 GenericCloseControl.qml Home 1.0 Home.qml NowPlayingHomeBar 1.0 NowPlayingHomeBar.qml OCPSkillsView 1.0 OCPSkillsView.qml -OVOSAudioPlayer 1.0 OVOSAudioPlayer.qml OVOSSeekControl 1.0 OVOSSeekControl.qml OVOSSeekControlQtAv 1.0 OVOSSeekControlQtAv.qml OVOSSyncPlayer 1.0 OVOSSyncPlayer.qml @@ -18,7 +17,6 @@ OVOSWebPlayer 1.0 OVOSWebPlayer.qml PlayerLoader 1.0 PlayerLoader.qml Playlist 1.0 Playlist.qml Search 1.0 Search.qml -SpectrumWaveDelegate 1.0 SpectrumWaveDelegate.qml SuggestionsView 1.0 SuggestionsView.qml VideoPlayerControl 1.0 VideoPlayerControl.qml views views diff --git a/ovos_plugin_common_play/ocp/search.py b/ovos_plugin_common_play/ocp/search.py index 7f5cdc6..e65ce94 100644 --- a/ovos_plugin_common_play/ocp/search.py +++ b/ovos_plugin_common_play/ocp/search.py @@ -1,320 +1,18 @@ -import random import time -from os.path import join, isfile -from threading import RLock, Lock -from typing import List +from threading import RLock 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_workshop.decorators.ocp import MediaType, PlaybackType, PlaybackMode +from ovos_workshop.decorators.ocp import MediaType 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 -class OCPQuery: - cast2audio = [ - MediaType.MUSIC, - MediaType.PODCAST, - MediaType.AUDIOBOOK, - MediaType.RADIO, - MediaType.RADIO_THEATRE, - MediaType.VISUAL_STORY, - MediaType.NEWS - ] - - def __init__(self, query, ocp_search=None, media_type=MediaType.GENERIC, bus=None): - LOG.debug(f"Created {media_type.name} query: {query}") - self.query = query - self.media_type = media_type - self.ocp_search = ocp_search - self._search_playlist = None - self._bus = bus - self.__dedicated_bus = False - self.reset() - - def bind(self, bus=None): - bus = bus or self._bus - if not bus: - self.__dedicated_bus = True - bus = get_mycroft_bus() - self._bus = bus - - def reset(self): - self.active_skills = {} - self.active_skills_lock = Lock() - self.query_replies = [] - self.searching = False - self.search_start = 0 - self.query_timeouts = self.settings.get("min_timeout", 5) - if self.settings.get("playback_mode") in [PlaybackMode.FORCE_AUDIOSERVICE, PlaybackMode.AUDIO_ONLY]: - self.has_gui = False - else: - self.has_gui = is_gui_running() or is_gui_connected(self.bus) - - @property - def settings(self) -> dict: - if self.ocp_search: - return self.ocp_search.settings - - default_path = join(get_xdg_config_save_path(), 'apps', - OCP_ID, 'settings.json') - if isfile(default_path): - from json_database import JsonStorage - return JsonStorage(default_path, disable_lock=True) - return dict() - - @property - def search_playlist(self) -> Playlist: - if self.ocp_search: - return self.ocp_search.search_playlist - if self._search_playlist is None: - self._search_playlist = Playlist() - return self._search_playlist - - @property - def bus(self): - if self._bus: - return self._bus - if self.ocp_search: - return self.ocp_search.bus - - @property - def gui(self): - if self.ocp_search: - return self.ocp_search.gui - - def send(self): - self.query_replies = [] - self.query_timeouts = self.settings.get("min_timeout", 5) - self.search_start = time.time() - self.searching = True - self.register_events() - self.bus.emit(Message('ovos.common_play.query', - {"phrase": self.query, - "question_type": self.media_type})) - - def wait(self): - # if there is no match type defined, lets increase timeout a bit - # since all skills need to search - if self.media_type == MediaType.GENERIC: - timeout = self.settings.get("max_timeout", 15) + 3 # timeout bonus - else: - timeout = self.settings.get("max_timeout", 15) - while self.searching and time.time() - self.search_start <= timeout: - time.sleep(0.1) - self.searching = False - self.remove_events() - - @property - def results(self) -> List[dict]: - return [s for s in self.query_replies if s.get("results")] - - def register_events(self): - LOG.debug("Registering Search Bus Events") - self.bus.on("ovos.common_play.skill.search_start", - self.handle_skill_search_start) - self.bus.on("ovos.common_play.skill.search_end", - self.handle_skill_search_end) - self.bus.on("ovos.common_play.query.response", - self.handle_skill_response) - - def remove_events(self): - LOG.debug("Removing Search Bus Events") - self.bus.remove_all_listeners("ovos.common_play.skill.search_start") - self.bus.remove_all_listeners("ovos.common_play.skill.search_end") - self.bus.remove_all_listeners("ovos.common_play.query.response") - - def __enter__(self): - """ Context handler, registers bus events """ - self.bind() - return self - - def __exit__(self, _type, value, traceback): - """ Removes the bus events """ - self.close() - - def close(self): - self.remove_events() - if self._bus and self.__dedicated_bus: - self._bus.close() - self._bus = None - - def handle_skill_search_start(self, message): - skill_id = message.data["skill_id"] - LOG.debug(f"{message.data['skill_id']} is searching") - with self.active_skills_lock: - if skill_id not in self.active_skills: - self.active_skills[skill_id] = Lock() - - def handle_skill_response(self, message): - search_phrase = message.data["phrase"] - if search_phrase != self.query: - # not an answer for this search query - return - timeout = message.data.get("timeout") - skill_id = message.data['skill_id'] - # LOG.debug(f"OVOSCommonPlay result: {skill_id}") - - # in case this handler fires before the search start handler - with self.active_skills_lock: - if skill_id not in self.active_skills: - self.active_skills[skill_id] = Lock() - with self.active_skills[skill_id]: - if message.data.get("searching"): - # extend the timeout by N seconds - if timeout and self.settings.get("allow_extensions", True): - self.query_timeouts += timeout - # else -> expired search - - else: - # Collect replies until the timeout - if not self.searching and not len(self.query_replies): - LOG.debug(" too late!! ignored in track selection process") - LOG.warning( - f"{message.data['skill_id']} is not answering fast " - "enough!") - - # populate search playlist - results = message.data.get("results", []) - for idx, res in enumerate(results): - if self.media_type not in [MediaType.ADULT, MediaType.HENTAI]: - # skip adult content results unless explicitly enabled - if not self.settings.get("adult_content", False) and \ - res.get("media_type", MediaType.GENERIC) in \ - [MediaType.ADULT, MediaType.HENTAI]: - continue - - # filter uris we can play, usually files and http streams, but some - # skills might return results that depend on additional packages, - # eg. soundcloud, rss, youtube, deezer.... - uri = res.get("uri", "") - if res.get("playlist") and not uri: - res["playlist"] = [ - r for r in res["playlist"] - if r.get("uri") and any(r.get("uri").startswith(e) - for e in - available_extractors())] - if not len(res["playlist"]): - results[idx] = None # can't play this search result! - LOG.error(f"Empty playlist for {res}") - continue - elif uri and res.get("playback") not in [ - PlaybackType.SKILL, PlaybackType.UNDEFINED] and \ - not any( - uri.startswith(e) for e in available_extractors()): - results[idx] = None # can't play this search result! - LOG.error(f"stream handler not available for {res}") - continue - - # filter video results if GUI not connected - if not self.has_gui: - # force allowed stream types to be played audio only - if res.get("media_type", "") in self.cast2audio: - LOG.debug("unable to use GUI, " - "forcing result to play audio only") - res["playback"] = PlaybackType.AUDIO - res["match_confidence"] -= 10 - results[idx] = res - - if res not in self.search_playlist: - self.search_playlist.add_entry(res) - # update search UI - if self.gui and self.searching and res["match_confidence"] >= 30: - if self.gui.active_extension == "smartspeaker": - self.gui.display_notification(f"Found some results for {res['title']}") - else: - self.gui["footer_text"] = \ - f"skill - {skill_id}\n" \ - f"match - {res['title']}\n" \ - f"confidence - {res['match_confidence']} " - - # remove filtered results - message.data["results"] = [r for r in results if r is not None] - LOG.debug(f'got {len(message.data["results"])} results from {skill_id}') - self.query_replies.append(message.data) - - # abort searching if we gathered enough results - # TODO ensure we have a decent confidence match, if all matches - # are < 50% conf extend timeout instead - if time.time() - self.search_start > self.query_timeouts: - if self.searching: - self.searching = False - LOG.debug("common play query timeout, parsing results") - if self.gui: - if self.gui.active_extension == "smartspeaker": - self.gui.display_notification("Parsing your results") - else: - self.gui["footer_text"] = "Timeout!\n " \ - "selecting best result\n" \ - " " - - elif self.searching: - for res in message.data.get("results", []): - if res.get("match_confidence", 0) >= \ - self.settings.get("early_stop_thresh", 85): - # got a really good match, dont search further - LOG.info( - "Receiving very high confidence match, stopping " - "search early") - if self.gui: - if self.gui.active_extension == "smartspeaker": - self.gui.display_notification("Found a great match, stopping search") - else: - self.gui["footer_text"] = \ - f"High confidence match!\n " \ - f"skill - {skill_id}\n" \ - f"match - {res['title']}\n" \ - f"confidence - {res['match_confidence']} " - # allow other skills to "just miss" - early_stop_grace = \ - self.settings.get("early_stop_grace_period", 0.5) - if early_stop_grace: - LOG.debug( - f" - grace period: {early_stop_grace} seconds") - time.sleep(early_stop_grace) - self.searching = False - return - - def handle_skill_search_end(self, message): - skill_id = message.data["skill_id"] - LOG.debug(f"{message.data['skill_id']} finished search") - with self.active_skills_lock: - if skill_id in self.active_skills: - with self.active_skills[skill_id]: - del self.active_skills[skill_id] - - # if this was the last skill end searching period - time.sleep(0.5) - # TODO this sleep is hacky, but avoids a race condition in - # case some skill just decides to respond before the others even - # acknowledge search is starting, this gives more than enough time - # for self.active_skills to be populated, a better approach should - # be employed but this works fine for now - if not self.active_skills and self.searching: - LOG.info("Received search responses from all skills!") - if self.gui: - if self.gui.active_extension == "smartspeaker": - self.gui.display_notification("Selecting best result") - else: - self.gui["footer_text"] = "Received search responses from all " \ - "skills!\nselecting best result" - - self.searching = False - if self.gui: - self.gui.update_search_results() - - class OCPSearch(OCPAbstractComponent): def __init__(self, player=None): # OCPMediaPlayer super(OCPSearch, self).__init__(player) self.search_playlist = Playlist() - self.old_cps = None self.ocp_skills = {} self.featured_skills = {} self.search_lock = RLock() @@ -323,13 +21,15 @@ def __init__(self, player=None): # OCPMediaPlayer def bind(self, player): # OCPMediaPlayer self._player = player - self.old_cps = None - if self.old_cps: - self.old_cps.bind(player) self.add_event("ovos.common_play.skills.detach", self.handle_ocp_skill_detach) self.add_event("ovos.common_play.announce", self.handle_skill_announce) + self.add_event("ovos.common_play.search.start", + self.handle_search_start) + + def handle_search_start(self, message): + self.gui.notify_search_status("Searching...") def shutdown(self): self.remove_event("ovos.common_play.announce") @@ -373,99 +73,6 @@ def get_featured_skills(self, adult=False): if MediaType.ADULT not in s["media_type"] and MediaType.HENTAI not in s["media_type"]] - def search(self, phrase, media_type=MediaType.GENERIC): - with self.search_lock: - # stop any search still happening - self.bus.emit(Message("ovos.common_play.search.stop")) - if self.gui: - if self.gui.active_extension == "smartspeaker": - self.gui.display_notification("Searching...Your query is being processed") - else: - if self.gui.persist_home_display: - self.gui.show_search_spinner(persist_home=True) - else: - self.gui.show_search_spinner(persist_home=False) - self.clear() - - query = OCPQuery(query=phrase, media_type=media_type, ocp_search=self, - bus=self.bus) - query.send() - - # old common play will send the messages expected by the official - # mycroft stack, but skills are known to over match, dont support - # match type, and the GUI can be different for every skill, it may also - # cause issues with status tracking and mess up playlists. An - # imperfect compatibility layer has been implemented at skill and - # audioservice level - if self.old_cps: - self.old_cps.send_query(phrase, media_type) - - query.wait() - - # fallback to generic search type - if not query.results and \ - self.settings.get("search_fallback", True) and \ - media_type != MediaType.GENERIC: - LOG.debug("OVOSCommonPlay falling back to MediaType.GENERIC") - query.media_type = MediaType.GENERIC - query.reset() - query.send() - query.wait() - - if self.gui: - self.gui.update_search_results() - LOG.debug(f'Returning {len(query.results)} search results') - return query.results - - def search_skill(self, skill_id, phrase, - media_type=MediaType.GENERIC): - res = [r for r in self.search(phrase, media_type) - if r["skill_id"] == skill_id] - if not len(res): - return None - return res[0] - - def select_best(self, results): - # Look at any replies that arrived before the timeout - # Find response(s) with the highest confidence - best = None - ties = [] - - for res in results: - if not best or res['match_confidence'] > best['match_confidence']: - best = res - ties = [best] - elif res['match_confidence'] == best['match_confidence']: - ties.append(res) - - if ties: - # select randomly - selected = random.choice(ties) - - if self.settings.get("playback_mode") == PlaybackMode.VIDEO_ONLY: - # select only from VIDEO results if preference is set - gui_results = [r for r in ties if r["playback"] == - PlaybackType.VIDEO] - if len(gui_results): - selected = random.choice(gui_results) - else: - return None - elif self.settings.get("playback_mode") == PlaybackMode.AUDIO_ONLY: - # select only from AUDIO results if preference is set - audio_results = [r for r in ties if r["playback"] != - PlaybackType.VIDEO] - if len(audio_results): - selected = random.choice(audio_results) - else: - return None - - # TODO: Ask user to pick between ties or do it automagically - else: - selected = best - LOG.debug(f"OVOSCommonPlay selected: {selected['skill_id']} - " - f"{selected['match_confidence']}") - return selected - def clear(self): self.search_playlist.clear() if self.gui: diff --git a/requirements/requirements_extra.txt b/requirements/requirements_extra.txt index b37cc4e..d3c0235 100644 --- a/requirements/requirements_extra.txt +++ b/requirements/requirements_extra.txt @@ -1,7 +1,5 @@ ovos-ocp-youtube-plugin>=0.0.1, < 1.0.0 -ovos-ocp-deezer-plugin>=0.0.1, < 1.0.0 ovos-ocp-rss-plugin>=0.0.2, < 1.0.0 -ovos-ocp-bandcamp-plugin>=0.0.1, < 1.0.0 ovos-ocp-m3u-plugin>=0.0.1, < 1.0.0 ovos-ocp-news-plugin>=0.0.3, < 1.0.0 ovos_audio_plugin_simple>=0.0.1, < 1.0.0 diff --git a/test/unittests/test_mycroft_bus_api.py b/test/unittests/test_mycroft_bus_api.py deleted file mode 100644 index 5950aae..0000000 --- a/test/unittests/test_mycroft_bus_api.py +++ /dev/null @@ -1,283 +0,0 @@ -import json -import unittest -from unittest.mock import patch - -from ovos_audio.service import AudioService -from ovos_config import Configuration -from ovos_utils.messagebus import FakeBus - -from ovos_plugin_common_play.ocp.mycroft_cps import MycroftAudioService -from ovos_plugin_common_play.ocp.status import PlayerState, MediaState, TrackState, PlaybackType - -BASE_CONF = {"Audio": - { - "native_sources": ["debug_cli", "audio"], - "default-backend": "OCP", # only used by mycroft-core - "preferred_audio_services": ["ovos_test", "mycroft_test"], - "backends": { - "OCP": { - "type": "ovos_common_play", - "active": True, - "mode": "auto", - "disable_mpris": True - }, - "mycroft_test": { - "type": "mycroft_test", - "active": True - }, - "ovos_test": { - "type": "ovos_test", - "active": True - } - } - } -} - - -class TestAudioServiceApi(unittest.TestCase): - bus = FakeBus() - - @classmethod - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def setUpClass(cls) -> None: - cls.bus.emitted_msgs = [] - - def get_msg(msg): - msg = json.loads(msg) - msg.pop("context") - cls.bus.emitted_msgs.append(msg) - - cls.bus.on("message", get_msg) - - cls.api = MycroftAudioService(cls.bus) - - @unittest.skip("debug - github actions gets stuck forever here ? works on my machine") - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def test_ocp_plugin_compat_layer(self): - audio = AudioService(self.bus) - self.bus.emitted_msgs = [] - - # test play track from single uri - test_uri = "file://path/to/music.mp3" - self.api.play([test_uri]) - expected = [ - {'type': 'mycroft.audio.service.play', - 'data': {'tracks': [test_uri], - 'utterance': '', 'repeat': False}}, - {'type': 'ovos.common_play.playlist.clear', 'data': {}}, - {'type': 'ovos.common_play.media.state', 'data': {'state': 3}}, - {'type': 'ovos.common_play.track.state', 'data': {'state': 31}}, - {'type': 'ovos.common_play.playlist.queue', - 'data': { - 'tracks': [{'uri': test_uri, - 'title': 'music.mp3', 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface'}]}}, - {'type': 'ovos.common_play.play', - 'data': { - 'repeat': False, - 'media': { - 'uri': test_uri, - 'title': 'music.mp3', 'playback': 2, 'status': 1, 'skill_id': 'mycroft.audio_interface', - 'skill': 'mycroft.audio_interface', 'position': 0, 'length': None, 'skill_icon': None, - 'artist': None, 'is_cps': False, 'cps_data': {}}, - 'playlist': [ - {'uri': test_uri, - 'title': 'music.mp3', 'playback': 2, 'status': 1, 'skill_id': 'mycroft.audio_interface', - 'skill': 'mycroft.audio_interface', 'position': 0, 'length': None, 'skill_icon': None, - 'artist': None, 'is_cps': False, 'cps_data': {}}]}} - ] - for m in expected: - self.assertIn(m, self.bus.emitted_msgs) - - # test pause - self.bus.emitted_msgs = [] - self.api.pause() - expected = [ - {'type': 'mycroft.audio.service.pause', 'data': {}}, - {'type': 'ovos.common_play.pause', 'data': {}} - ] - for m in expected: - self.assertIn(m, self.bus.emitted_msgs) - - # test resume - self.bus.emitted_msgs = [] - self.api.resume() - expected = [ - {'type': 'mycroft.audio.service.resume', 'data': {}}, - {'type': 'ovos.common_play.resume', 'data': {}} - ] - for m in expected: - self.assertIn(m, self.bus.emitted_msgs) - - # test next - self.bus.emitted_msgs = [] - self.api.next() - expected = [ - {'type': 'mycroft.audio.service.next', 'data': {}}, - {'type': 'ovos.common_play.next', 'data': {}} - ] - for m in expected: - self.assertIn(m, self.bus.emitted_msgs) - - # test prev - self.bus.emitted_msgs = [] - self.api.prev() - expected = [ - {'type': 'mycroft.audio.service.prev', 'data': {}}, - {'type': 'ovos.common_play.previous', 'data': {}} - ] - for m in expected: - self.assertIn(m, self.bus.emitted_msgs) - - # test queue - self.bus.emitted_msgs = [] - playlist = ["file://path/to/music2.mp3", "file://path/to/music3.mp3"] - self.api.queue(playlist) - expected = [ - {'type': 'mycroft.audio.service.queue', - 'data': {'tracks': ['file://path/to/music2.mp3', 'file://path/to/music3.mp3']}}, - {'type': 'ovos.common_play.playlist.queue', - 'data': {'tracks': [ - {'uri': 'file://path/to/music2.mp3', 'title': 'music2.mp3', 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface'}, - {'uri': 'file://path/to/music3.mp3', 'title': 'music3.mp3', 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface'}] - }} - ] - for m in expected: - self.assertIn(m, self.bus.emitted_msgs) - - # test play playlist - self.bus.emitted_msgs = [] - self.api.play([test_uri] + playlist) - expected = [ - {'type': 'mycroft.audio.service.play', - 'data': {'tracks': ['file://path/to/music.mp3', 'file://path/to/music2.mp3', 'file://path/to/music3.mp3'], - 'utterance': '', - 'repeat': False}}, - {'type': 'ovos.common_play.playlist.queue', - 'data': {'tracks': [ - {'uri': 'file://path/to/music.mp3', 'title': 'music.mp3', 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface'}, - {'uri': 'file://path/to/music2.mp3', 'title': 'music2.mp3', 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface'}, - {'uri': 'file://path/to/music3.mp3', 'title': 'music3.mp3', 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface'}]}}, - {'type': 'ovos.common_play.play', - 'data': {'repeat': False, - 'media': {'uri': 'file://path/to/music.mp3', - 'title': 'music.mp3', 'playback': 2, - 'status': 1, - 'skill_id': 'mycroft.audio_interface', - 'skill': 'mycroft.audio_interface', - 'position': 0, 'length': None, - 'skill_icon': None, 'artist': None, - 'is_cps': False, 'cps_data': {}}, - 'playlist': [ - {'uri': 'file://path/to/music.mp3', 'title': 'music.mp3', - 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface', - 'skill': 'mycroft.audio_interface', 'position': 0, - 'length': None, 'skill_icon': None, 'artist': None, - 'is_cps': False, 'cps_data': {}}, - {'uri': 'file://path/to/music2.mp3', 'title': 'music2.mp3', - 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface', - 'skill': 'mycroft.audio_interface', 'position': 0, - 'length': None, 'skill_icon': None, 'artist': None, - 'is_cps': False, 'cps_data': {}}, - {'uri': 'file://path/to/music3.mp3', 'title': 'music3.mp3', - 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface', - 'skill': 'mycroft.audio_interface', 'position': 0, - 'length': None, 'skill_icon': None, 'artist': None, - 'is_cps': False, 'cps_data': {}}]}} - ] - for m in expected: - self.assertIn(m, self.bus.emitted_msgs) - - audio.shutdown() - - @unittest.skip("debug - github actions gets stuck forever here ? works on my machine") - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def test_play_mycroft_backend(self): - audio = AudioService(self.bus) - self.bus.emitted_msgs = [] - selected = "mycroft_test" - tracks = ["file://path/to/music.mp3", "file://path/to/music2.mp3"] - - # assert OCP not in use - self.assertNotEqual(audio.default.ocp.player.state, PlayerState.PLAYING) - - self.api.play(tracks, repeat=True, utterance=selected) - - # correct service selected - self.assertEqual(audio.current.name, selected) - self.assertTrue(audio.current.playing) - - # OCP is not aware of internal player state - state events not emitted by mycroft plugins - self.assertNotEqual(audio.default.ocp.player.state, PlayerState.PLAYING) - - # but track state is partially accounted for - self.assertEqual(audio.default.ocp.player.now_playing.uri, tracks[0]) - self.assertEqual(audio.default.ocp.player.now_playing.playback, PlaybackType.AUDIO_SERVICE) - self.assertEqual(audio.default.ocp.player.now_playing.status, TrackState.QUEUED_AUDIOSERVICE) - self.assertEqual(audio.default.ocp.player.now_playing.skill_id, "mycroft.audio_interface") - - audio.current._track_start_callback("track_name") - self.assertEqual(audio.default.ocp.player.now_playing.status, TrackState.PLAYING_AUDIOSERVICE) - - audio.shutdown() - - @unittest.skip("debug - github actions gets stuck forever here ? works on my machine") - @patch.dict(Configuration._Configuration__patch, BASE_CONF) - def test_play_ocp_backend(self): - audio = AudioService(self.bus) - self.bus.emitted_msgs = [] - - selected = "ovos_test" - tracks = ["file://path/to/music.mp3", "file://path/to/music2.mp3"] - - # assert OCP not in use - self.assertNotEqual(audio.default.ocp.player.state, PlayerState.PLAYING) - - # NOTE: this usage is equivalent to what OCP itself - # does internally to select audio_service, where "utterance" is also used - self.api.play(tracks, repeat=True, utterance=selected) - - # correct service selected - self.assertEqual(audio.current.name, selected) - - # ocp state events emitted - exptected = [ - {'type': 'mycroft.audio.service.play', - 'data': {'tracks': ['file://path/to/music.mp3', 'file://path/to/music2.mp3'], 'utterance': 'ovos_test', - 'repeat': True}}, - {'type': 'ovos.common_play.playlist.clear', 'data': {}}, # TODO - maybe this is unwanted (?) - {'type': 'ovos.common_play.media.state', 'data': {'state': 3}}, - {'type': 'ovos.common_play.track.state', 'data': {'state': 31}}, - {'type': 'ovos.common_play.playlist.queue', 'data': {'tracks': [ - {'uri': 'file://path/to/music.mp3', 'title': 'music.mp3', 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface'}, - {'uri': 'file://path/to/music2.mp3', 'title': 'music2.mp3', 'playback': 2, 'status': 1, - 'skill_id': 'mycroft.audio_interface'}]}}, - {'type': 'ovos.common_play.repeat.set', 'data': {}}, - {'type': 'ovos.common_play.player.state', 'data': {'state': 1}}, - {'type': 'ovos.common_play.media.state', 'data': {'state': 3}}, - {'type': 'ovos.common_play.track.state', 'data': {'state': 21}} - ] - for m in exptected: - self.assertIn(m, self.bus.emitted_msgs) - - # assert OCP is tracking state - self.assertEqual(audio.default.ocp.player.state, PlayerState.PLAYING) - self.assertEqual(audio.default.ocp.player.media_state, MediaState.LOADED_MEDIA) - self.assertEqual(audio.default.ocp.player.now_playing.uri, tracks[0]) - self.assertEqual(audio.default.ocp.player.now_playing.playback, PlaybackType.AUDIO_SERVICE) - self.assertEqual(audio.default.ocp.player.now_playing.status, TrackState.PLAYING_AUDIOSERVICE) - - audio.shutdown() - - -if __name__ == '__main__': - unittest.main() diff --git a/test/unittests/test_ocp.py b/test/unittests/test_ocp.py index 348ce0f..97dddb1 100644 --- a/test/unittests/test_ocp.py +++ b/test/unittests/test_ocp.py @@ -32,7 +32,6 @@ def test_00_ocp_init(self): self.assertIsInstance(self.ocp.gui, OCPMediaPlayerGUI) self.assertIsInstance(self.ocp.settings, dict) self.assertIsInstance(self.ocp.player, OCPMediaPlayer) - self.assertIsNotNone(self.ocp.media_intents) # Mock startup events def _handle_skills_check(msg): @@ -41,7 +40,6 @@ def _handle_skills_check(msg): self.bus.once('mycroft.skills.is_ready', _handle_skills_check) self.bus.emit(Message('mycroft.ready')) - def test_ping(self): resp = self.bus.wait_for_response(Message("ovos.common_play.ping"), reply_type="ovos.common_play.pong") @@ -62,132 +60,10 @@ def test_register_media_events(self): # TODO pass - def test_replace_mycroft_cps(self): - # TODO - pass - def test_default_shutdown(self): # TODO pass - def test_classify_media(self): - music = "play some music" - 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) - self.assertEqual(self.ocp.classify_media(unknown), MediaType.GENERIC) - - def test_handle_open(self): - real_gui_home = self.ocp.gui.show_home - self.ocp.gui.show_home = Mock() - self.ocp.handle_open(None) - self.ocp.gui.show_home.assert_called_once_with(app_mode=True) - self.ocp.gui.show_home = real_gui_home - - def test_handle_playback_intents(self): - real_player = self.ocp.player - self.ocp.player = MagicMock() - - # next - self.ocp.handle_next(None) - self.ocp.player.play_next.assert_called_once() - - # previous - self.ocp.handle_prev(None) - self.ocp.player.play_prev.assert_called_once() - - # pause - self.ocp.handle_pause(None) - self.ocp.player.pause.assert_called_once() - - # stop - self.ocp.handle_stop() - self.ocp.player.stop.assert_called_once() - - # resume - self.ocp.player.state = PlayerState.PAUSED - self.ocp.handle_resume(None) - self.ocp.player.resume.assert_called_once() - - # resume while playing - self.ocp.player.state = PlayerState.PLAYING - real_get_response = self.ocp.get_response - real_play = self.ocp.handle_play - self.ocp.get_response = Mock(return_value="test") - self.ocp.handle_play = Mock() - - test_message = Message("test") - self.ocp.handle_resume(test_message) - self.ocp.get_response.assert_called_once_with("play.what") - self.ocp.handle_play.assert_called_once_with(test_message) - self.assertEqual(test_message.data['utterance'], 'test') - - self.ocp.handle_play = real_play - self.ocp.get_response = real_get_response - self.ocp.player = real_player - - def test_handle_play(self): - # TODO - pass - - def test_handle_read(self): - # TODO - pass - - def test_do_play(self): - called = False - - def play_media(*args, **kwargs): - nonlocal called - called = True - - self.ocp.player.play_media = play_media - - msg = Message("") - self.ocp.player.handle_play_request(msg) - self.assertTrue(called) # no message.context -> broadcast for everyone - - msg = Message("", {}, {"destination": "audio"}) - called = False - self.ocp.player.handle_play_request(msg) - self.assertTrue(called) # "audio" is a native source - - msg = Message("", {}, {"destination": "hive"}) - called = False - self.ocp.player.handle_play_request(msg) - self.assertFalse(called) # ignored playback for remote client - - def test_search(self): - # TODO - pass - - def test_should_resume(self): - valid_utt = "resume" - invalid_utt = "test" - empty_utt = "" - - # Playing - self.ocp.player.state = PlayerState.PLAYING - self.assertFalse(self.ocp._should_resume(valid_utt)) - self.assertFalse(self.ocp._should_resume(invalid_utt)) - self.assertFalse(self.ocp._should_resume(empty_utt)) - - # Stopped - self.ocp.player.state = PlayerState.STOPPED - self.assertFalse(self.ocp._should_resume(valid_utt)) - self.assertFalse(self.ocp._should_resume(invalid_utt)) - self.assertFalse(self.ocp._should_resume(empty_utt)) - - # Paused - self.ocp.player.state = PlayerState.PAUSED - self.assertTrue(self.ocp._should_resume(valid_utt)) - self.assertFalse(self.ocp._should_resume(invalid_utt)) - self.assertTrue(self.ocp._should_resume(empty_utt)) - if __name__ == "__main__": unittest.main() diff --git a/test/unittests/test_ocp_player.py b/test/unittests/test_ocp_player.py index b80166d..3d9185a 100644 --- a/test/unittests/test_ocp_player.py +++ b/test/unittests/test_ocp_player.py @@ -57,7 +57,7 @@ def test_00_player_init(self): from ovos_plugin_common_play.ocp.search import OCPSearch from ovos_plugin_common_play.ocp.media import NowPlaying, Playlist from ovos_plugin_common_play.ocp.mpris import MprisPlayerCtl - from ovos_plugin_common_play.ocp.mycroft_cps import MycroftAudioService + from ovos_bus_client.apis.ocp import ClassicAudioServiceInterface from ovos_workshop import OVOSAbstractApplication self.assertIsInstance(self.player, OVOSAbstractApplication) @@ -79,7 +79,7 @@ def test_00_player_init(self): self.assertEqual(self.player.now_playing._player, self.player) self.assertEqual(self.player.media._player, self.player) self.assertEqual(self.player.gui.player, self.player) - self.assertIsInstance(self.player.audio_service, MycroftAudioService) + self.assertIsInstance(self.player.audio_service, ClassicAudioServiceInterface) bus_events = ['recognizer_loop:record_begin', 'recognizer_loop:record_end', @@ -274,8 +274,7 @@ def test_set_now_playing(self): self.player.gui.update_current_track = real_update_track self.player.gui.update_playlist = real_update_plist - @patch("ovos_plugin_common_play.ocp.player.is_gui_running") - def test_validate_stream(self, gui_running): + def test_validate_stream(self): real_update = self.player.gui.update_current_track self.player.gui.update_current_track = Mock() media_entry = MediaEntry.from_dict(valid_search_results[0]) @@ -292,12 +291,11 @@ def test_validate_stream(self, gui_running): self.assertEqual(self.player.active_backend, PlaybackType.AUDIO) # Test with GUI - gui_running.return_value = True self.assertTrue(self.player.validate_stream()) self.assertEqual(self.player.gui["stream"], media_entry.uri) self.player.gui.update_current_track.assert_called_once() self.assertEqual(self.player.now_playing.playback, - PlaybackType.AUDIO) + PlaybackType.AUDIO_SERVICE) # Invalid Entry self.player.now_playing.update(invalid_entry) @@ -306,7 +304,6 @@ def test_validate_stream(self, gui_running): self.player.gui.update_current_track.assert_called_once() # Test without GUI - gui_running.return_value = False self.player.gui.update_current_track.reset_mock() self.player.gui["stream"] = None self.player.now_playing.update(media_entry) @@ -416,9 +413,7 @@ def test_get_preferred_audio_backend(self): self.assertIn(preferred, ["mpv", "ovos_common_play", "vlc", "mplayer", "simple"]) - @patch("ovos_plugin_common_play.ocp.player.is_gui_running") - def test_play(self, gui_running): - gui_running.return_value = True + def test_play(self): real_update_props = self.player.mpris.update_props real_stop = self.player.mpris.stop real_validate_stream = self.player.validate_stream @@ -477,19 +472,21 @@ def test_play(self, gui_running): last_message = self.emitted_msgs[-1] second_last_message = self.emitted_msgs[-2] self.assertEqual(last_message.msg_type, "ovos.common_play.track.state") - self.assertEqual(last_message.data, {"state": TrackState.PLAYING_AUDIO}) + self.assertEqual(last_message.data, {"state": TrackState.PLAYING_AUDIOSERVICE}) self.assertEqual(second_last_message.msg_type, - "gui.player.media.service.play") + "gui.player.media.service.set.meta") self.assertEqual(second_last_message.data, - {"track": media.uri, "mime": list(media.mimetype), - "repeat": False}) + {'title': 'Orbiting A Distant Planet', + 'image': 'https://freemusicarchive.org/legacy/fma-smaller.jpg', + 'artist': 'Quantum Jazz'}) + + self.player.mpris.stop.reset_mock() self.player.validate_stream.reset_mock() self.player.gui.show_player.reset_mock() # Test valid audio without gui (AudioService) - gui_running.return_value = False self.player.mpris.stop_event.clear() self.player.play() self.player.mpris.stop.assert_called_once()