From f2218de2b66730b0af49ba3c8b0d8ed9dd6226ea Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 11 Oct 2020 01:39:06 +0200 Subject: [PATCH] small fixes --- music_assistant/mass.py | 18 +- .../{mass => builtin_player}/__init__.py | 210 ++---------------- .../{mass => builtin_player}/icon.png | Bin music_assistant/web/endpoints/websocket.py | 13 +- 4 files changed, 31 insertions(+), 210 deletions(-) rename music_assistant/providers/{mass => builtin_player}/__init__.py (55%) rename music_assistant/providers/{mass => builtin_player}/icon.png (100%) diff --git a/music_assistant/mass.py b/music_assistant/mass.py index 0c0b5891..cfdab9dc 100644 --- a/music_assistant/mass.py +++ b/music_assistant/mass.py @@ -30,6 +30,14 @@ from zeroconf import NonUniqueNameException, ServiceInfo, Zeroconf LOGGER = logging.getLogger("mass") +def global_exception_handler(loop: asyncio.AbstractEventLoop, context: Dict) -> None: + """Global exception handler.""" + LOGGER.exception( + "Caught exception: %s", context.get("exception", context["message"]) + ) + loop.default_exception_handler(context) + + class MusicAssistant: """Main MusicAssistant object.""" @@ -63,7 +71,7 @@ class MusicAssistant: """Start running the music assistant server.""" # initialize loop self._loop = asyncio.get_event_loop() - self._loop.set_exception_handler(__handle_exception) + self._loop.set_exception_handler(global_exception_handler) self._loop.set_debug(self._debug) # create shared aiohttp ClientSession self._http_session = aiohttp.ClientSession( @@ -334,11 +342,3 @@ class MusicAssistant: LOGGER.exception("Error preloading module %s: %s", module_name, exc) else: LOGGER.debug("Successfully preloaded module %s", module_name) - - -def __handle_exception(loop: asyncio.AbstractEventLoop, context: Dict) -> None: - """Global exception handler.""" - LOGGER.exception( - "Caught exception: %s", context.get("exception", context["message"]) - ) - loop.default_exception_handler(context) diff --git a/music_assistant/providers/mass/__init__.py b/music_assistant/providers/builtin_player/__init__.py similarity index 55% rename from music_assistant/providers/mass/__init__.py rename to music_assistant/providers/builtin_player/__init__.py index 383bcc15..dacc85fd 100644 --- a/music_assistant/providers/mass/__init__.py +++ b/music_assistant/providers/builtin_player/__init__.py @@ -1,13 +1,10 @@ """Builtin player provider.""" -import asyncio import logging -import signal -import subprocess import time from typing import List from music_assistant.helpers.typing import MusicAssistantType -from music_assistant.helpers.util import get_hostname, run_periodic +from music_assistant.helpers.util import run_periodic from music_assistant.models.config_entry import ConfigEntry from music_assistant.models.player import ( DeviceInfo, @@ -17,9 +14,9 @@ from music_assistant.models.player import ( ) from music_assistant.models.provider import PlayerProvider -PROV_ID = "mass" +PROV_ID = "builtin_player" PROV_NAME = "Music Assistant" -LOGGER = logging.getLogger("mass_provider") +LOGGER = logging.getLogger(PROV_ID) CONFIG_ENTRIES = [] PLAYER_CONFIG_ENTRIES = [] @@ -40,7 +37,6 @@ class MassPlayerProvider(PlayerProvider): """ Built-in PlayerProvider. - Provides a single headless local player on the server using SoX. Provides virtual players in the frontend using websockets. """ @@ -61,9 +57,6 @@ class MassPlayerProvider(PlayerProvider): async def async_on_start(self) -> bool: """Handle initialization of the provider based on config.""" - # add local sox player on the server - player = BuiltinLocalPlayer("server_player", f"Server: {get_hostname()}") - self.mass.add_job(self.mass.players.async_add_player(player)) # listen for websockets events to dynamically create players self.mass.add_event_listener( self.async_handle_mass_event, @@ -101,186 +94,6 @@ class MassPlayerProvider(PlayerProvider): for player_id in offline_players: await self.mass.players.async_remove_player(player_id) - async def __async_handle_player_state(self, data): - """Handle state event from player.""" - player_id = data["player_id"] - player = self.mass.players.get_player(player_id) - if "volume_level" in data: - player.volume_level = data["volume_level"] - if "muted" in data: - player.muted = data["muted"] - if "state" in data: - player.state = PlaybackState(data["state"]) - if "cur_time" in data: - player.elapsed_time = data["elapsed_time"] - if "current_uri" in data: - player.current_uri = data["current_uri"] - if "powered" in data: - player.powered = data["powered"] - if "name" in data: - player.name = data["name"] - player.last_message = time.time() - player.update_state() - - -class BuiltinLocalPlayer(Player): - """Representation of a local player on the server using SoX.""" - - def __init__(self, player_id: str, name: str) -> None: - """Initialize the built-in player.""" - self._player_id = player_id - self._name = name - self._powered = False - self._elapsed_time = 0 - self._state = PlaybackState.Stopped - self._current_uri = "" - self._volume_level = 100 - self._muted = False - self._sox = None - self._progress_task = None - - @property - def player_id(self) -> str: - """Return player id of this player.""" - return self._player_id - - @property - def provider_id(self) -> str: - """Return provider id of this player.""" - return PROV_ID - - @property - def name(self) -> str: - """Return name of the player.""" - return self._name - - @property - def powered(self) -> bool: - """Return current power state of player.""" - return self._powered - - @property - def elapsed_time(self) -> float: - """Return elapsed_time of current playing uri in seconds.""" - return self._elapsed_time - - @property - def state(self) -> PlaybackState: - """Return current PlaybackState of player.""" - return self._state - - @property - def available(self) -> bool: - """Return current availablity of player.""" - return True - - @property - def current_uri(self) -> str: - """Return currently loaded uri of player (if any).""" - return self._current_uri - - @property - def volume_level(self) -> int: - """Return current volume level of player (scale 0..100).""" - return self._volume_level - - @property - def muted(self) -> bool: - """Return current mute state of player.""" - return self._muted - - @property - def is_group_player(self) -> bool: - """Return True if this player is a group player.""" - return False - - @property - def device_info(self) -> DeviceInfo: - """Return the device info for this player.""" - return DeviceInfo( - model="Demo", address="http://demo:12345", manufacturer=PROV_NAME - ) - - # SERVICE CALLS / PLAYER COMMANDS - - async def async_cmd_play_uri(self, uri: str): - """Play the specified uri/url on the player.""" - if self._sox: - await self.async_cmd_stop() - self._current_uri = uri - self._sox = subprocess.Popen(["play", "-t", "flac", "-q", uri]) - self._state = PlaybackState.Playing - self._powered = True - self.update_state() - - async def report_progress(): - """Report fake progress while sox is playing.""" - LOGGER.info("Playback started on player %s", self.name) - self._elapsed_time = 0 - while self._sox and not self._sox.poll(): - await asyncio.sleep(1) - self._elapsed_time += 1 - self.update_state() - LOGGER.info("Playback stopped on player %s", self.name) - self._elapsed_time = 0 - self._state = PlaybackState.Stopped - self.update_state() - - if self._progress_task: - self._progress_task.cancel() - self._progress_task = self.mass.add_job(report_progress) - - async def async_cmd_stop(self) -> None: - """Send STOP command to player.""" - if self._sox: - self._sox.terminate() - self._sox = None - self._state = PlaybackState.Stopped - self.update_state() - - async def async_cmd_play(self) -> None: - """Send PLAY command to player.""" - if self._sox: - self._sox.send_signal(signal.SIGCONT) - self._state = PlaybackState.Playing - self.update_state() - - async def async_cmd_pause(self): - """Send PAUSE command to given player.""" - if self._sox: - self._sox.send_signal(signal.SIGSTOP) - self._state = PlaybackState.Paused - self.update_state() - - async def async_cmd_power_on(self) -> None: - """Send POWER ON command to player.""" - self._powered = True - self.update_state() - - async def async_cmd_power_off(self) -> None: - """Send POWER OFF command to player.""" - await self.async_cmd_stop() - self._powered = False - self.update_state() - - async def async_cmd_volume_set(self, volume_level: int) -> None: - """ - Send volume level command to given player. - - :param volume_level: volume level to set (0..100). - """ - self._volume_level = volume_level - self.update_state() - - async def async_cmd_volume_mute(self, is_muted=False): - """ - Send volume MUTE command to given player. - - :param is_muted: bool with new mute state. - """ - self._muted = is_muted - self.update_state() - class WebsocketsPlayer(Player): """ @@ -301,6 +114,7 @@ class WebsocketsPlayer(Player): self._current_uri = "" self._volume_level = 100 self._muted = False + self._device_info = DeviceInfo() self.last_message = time.time() async def handle_player_state(self, data: dict): @@ -315,10 +129,11 @@ class WebsocketsPlayer(Player): self._elapsed_time = data["elapsed_time"] if "current_uri" in data: self._current_uri = data["current_uri"] - if "powered" in data: - self._powered = data["powered"] if "name" in data: self._player_name = data["name"] + if "device_info" in data: + for key, value in data["device_info"].items(): + setattr(self._device_info, key, value) self.last_message = time.time() self.update_state() @@ -370,7 +185,7 @@ class WebsocketsPlayer(Player): @property def device_info(self) -> DeviceInfo: """Return the device info for this player.""" - return DeviceInfo() + return self._device_info @property def should_poll(self) -> bool: @@ -413,13 +228,14 @@ class WebsocketsPlayer(Player): async def async_cmd_power_on(self) -> None: """Send POWER ON command to player.""" - data = {"player_id": self.player_id, "cmd": "power_on"} - self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + self._powered = True + self.update_state() async def async_cmd_power_off(self) -> None: """Send POWER OFF command to player.""" - data = {"player_id": self.player_id, "cmd": "power_off"} - self.mass.signal_event(EVENT_WEBPLAYER_CMD, data) + await self.async_cmd_stop() + self._powered = False + self.update_state() async def async_cmd_volume_set(self, volume_level: int) -> None: """ diff --git a/music_assistant/providers/mass/icon.png b/music_assistant/providers/builtin_player/icon.png similarity index 100% rename from music_assistant/providers/mass/icon.png rename to music_assistant/providers/builtin_player/icon.png diff --git a/music_assistant/web/endpoints/websocket.py b/music_assistant/web/endpoints/websocket.py index ead069d9..9b40d64f 100644 --- a/music_assistant/web/endpoints/websocket.py +++ b/music_assistant/web/endpoints/websocket.py @@ -1,7 +1,6 @@ """Websocket API endpoint.""" import logging -from asyncio import CancelledError import jwt import orjson @@ -29,7 +28,14 @@ async def async_websocket_handler(request: Request): if hasattr(msg_details, "to_dict"): msg_details = msg_details.to_dict() ws_msg = {"message": msg, "message_details": msg_details} - await ws_response.send_str(json_serializer(ws_msg).decode()) + try: + await ws_response.send_str(json_serializer(ws_msg).decode()) + # pylint: disable=broad-except + except Exception as exc: + LOGGER.debug( + "Error while trying to send message to websocket (probably disconnected): %s", + str(exc), + ) # process incoming messages async for msg in ws_response: @@ -87,9 +93,8 @@ async def async_websocket_handler(request: Request): else: # simply echo the message on the eventbus request.app["mass"].signal_event(msg, msg_details) - except (AssertionError, CancelledError): - LOGGER.debug("Websocket disconnected") finally: + LOGGER.debug("Websocket disconnected") for remove_callback in remove_callbacks: remove_callback() return ws_response -- 2.34.1