From 492afb6b70f52dadcddc5e4eb59d2cde18090069 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 19 Mar 2023 02:30:49 +0100 Subject: [PATCH] Various small glitches resolved (#548) --- .../common/models/config_entries.py | 28 ++++- music_assistant/constants.py | 1 + music_assistant/server/controllers/config.py | 9 +- .../server/controllers/media/playlists.py | 6 +- .../server/controllers/media/tracks.py | 1 + music_assistant/server/controllers/streams.py | 11 +- music_assistant/server/helpers/audio.py | 4 +- music_assistant/server/models/provider.py | 7 +- .../server/providers/chromecast/__init__.py | 17 ++- .../server/providers/filesystem_local/base.py | 3 +- .../server/providers/frontend/manifest.json | 2 +- .../server/providers/json_rpc/manifest.json | 15 --- .../{json_rpc => lms_cli}/__init__.py | 100 +++++++++++++++++- .../server/providers/lms_cli/manifest.json | 14 +++ .../providers/{json_rpc => lms_cli}/models.py | 0 .../server/providers/slimproto/__init__.py | 98 +---------------- .../server/providers/slimproto/manifest.json | 5 +- .../server/providers/sonos/__init__.py | 2 +- requirements_all.txt | 2 +- 19 files changed, 188 insertions(+), 137 deletions(-) delete mode 100644 music_assistant/server/providers/json_rpc/manifest.json rename music_assistant/server/providers/{json_rpc => lms_cli}/__init__.py (70%) create mode 100644 music_assistant/server/providers/lms_cli/manifest.json rename music_assistant/server/providers/{json_rpc => lms_cli}/models.py (100%) diff --git a/music_assistant/common/models/config_entries.py b/music_assistant/common/models/config_entries.py index 433d14fa..62aff62f 100644 --- a/music_assistant/common/models/config_entries.py +++ b/music_assistant/common/models/config_entries.py @@ -15,6 +15,7 @@ from music_assistant.constants import ( CONF_EQ_MID, CONF_EQ_TREBLE, CONF_FLOW_MODE, + CONF_LOG_LEVEL, CONF_OUTPUT_CHANNELS, CONF_VOLUME_NORMALISATION, CONF_VOLUME_NORMALISATION_TARGET, @@ -109,6 +110,7 @@ class ConfigEntryValue(ConfigEntry): if not isinstance(result.value, expected_type): if result.value is None and allow_none: # In some cases we allow this (e.g. create default config) + result.value = result.default_value return result # handle common conversions/mistakes if expected_type == float and isinstance(result.value, int): @@ -190,6 +192,8 @@ class Config(DataClassDictMixin): for key in ("enabled", "name"): cur_val = getattr(self, key, None) new_val = getattr(update, key, None) + if new_val is None: + continue if new_val == cur_val: continue setattr(self, key, new_val) @@ -201,7 +205,10 @@ class Config(DataClassDictMixin): cur_val = self.values[key].value if cur_val == new_val: continue - self.values[key].value = new_val + if new_val is None: + self.values[key].value = self.values[key].default_value + else: + self.values[key].value = new_val changed_keys.add(f"values/{key}") return changed_keys @@ -254,6 +261,25 @@ class ConfigUpdate(DataClassDictMixin): values: dict[str, ConfigValueType] | None = None +DEFAULT_PROVIDER_CONFIG_ENTRIES = ( + ConfigEntry( + key=CONF_LOG_LEVEL, + type=ConfigEntryType.STRING, + label="Log level", + options=[ + ConfigValueOption("global", "GLOBAL"), + ConfigValueOption("info", "INFO"), + ConfigValueOption("warning", "WARNING"), + ConfigValueOption("error", "ERROR"), + ConfigValueOption("debug", "DEBIG"), + ], + default_value="GLOBAL", + description="Set the log verbosity for this provider", + advanced=True, + ), +) + + DEFAULT_PLAYER_CONFIG_ENTRIES = ( ConfigEntry( key=CONF_VOLUME_NORMALISATION, diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 71248efa..bf444718 100755 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -47,6 +47,7 @@ CONF_EQ_MID: Final[str] = "eq_mid" CONF_EQ_TREBLE: Final[str] = "eq_treble" CONF_OUTPUT_CHANNELS: Final[str] = "output_channels" CONF_FLOW_MODE: Final[str] = "flow_mode" +CONF_LOG_LEVEL: Final[str] = "log_level" # config default values DEFAULT_HOST: Final[str] = "0.0.0.0" diff --git a/music_assistant/server/controllers/config.py b/music_assistant/server/controllers/config.py index 83c7ab13..f3dd8145 100644 --- a/music_assistant/server/controllers/config.py +++ b/music_assistant/server/controllers/config.py @@ -16,6 +16,7 @@ from music_assistant.common.helpers.json import JSON_DECODE_EXCEPTIONS, json_dum from music_assistant.common.models import config_entries from music_assistant.common.models.config_entries import ( DEFAULT_PLAYER_CONFIG_ENTRIES, + DEFAULT_PROVIDER_CONFIG_ENTRIES, ConfigEntryValue, ConfigUpdate, PlayerConfig, @@ -155,10 +156,7 @@ class ConfigController: raw_values: dict[str, dict] = self.get(CONF_PROVIDERS, {}) prov_entries = {x.domain: x.config_entries for x in self.mass.get_available_providers()} return [ - ProviderConfig.parse( - prov_entries[prov_conf["domain"]], - prov_conf, - ) + self.get_provider_config(prov_conf["instance_id"]) for prov_conf in raw_values.values() if (provider_type is None or prov_conf["type"] == provider_type) and (provider_domain is None or prov_conf["domain"] == provider_domain) @@ -173,7 +171,8 @@ class ConfigController: for prov in self.mass.get_available_providers(): if prov.domain != raw_conf["domain"]: continue - return ProviderConfig.parse(prov.config_entries, raw_conf) + config_entries = DEFAULT_PROVIDER_CONFIG_ENTRIES + tuple(prov.config_entries) + return ProviderConfig.parse(config_entries, raw_conf) raise KeyError(f"No config found for provider id {instance_id}") @api_command("config/providers/update") diff --git a/music_assistant/server/controllers/media/playlists.py b/music_assistant/server/controllers/media/playlists.py index 9ecdcde0..7fc3f4cd 100644 --- a/music_assistant/server/controllers/media/playlists.py +++ b/music_assistant/server/controllers/media/playlists.py @@ -72,11 +72,11 @@ class PlaylistController(MediaControllerBase[Playlist]): ) return db_item - async def create(self, name: str, provider: str | None = None) -> Playlist: + async def create(self, name: str, provider_instance_or_domain: str | None = None) -> Playlist: """Create new playlist.""" # if provider is omitted, just pick first provider - if provider: - provider = self.mass.get_provider(provider) + if provider_instance_or_domain: + provider = self.mass.get_provider(provider_instance_or_domain) else: provider = next( ( diff --git a/music_assistant/server/controllers/media/tracks.py b/music_assistant/server/controllers/media/tracks.py index 46c867d5..6a56740c 100644 --- a/music_assistant/server/controllers/media/tracks.py +++ b/music_assistant/server/controllers/media/tracks.py @@ -43,6 +43,7 @@ class TracksController(MediaControllerBase[Track]): self.mass.register_api_command("music/track/versions", self.versions) self.mass.register_api_command("music/track/update", self.update_db_item) self.mass.register_api_command("music/track/delete", self.delete_db_item) + self.mass.register_api_command("music/track/preview", self.get_preview_url) async def get( self, diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py index 716ae3f9..a1d665c8 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/server/controllers/streams.py @@ -285,10 +285,13 @@ class StreamsController: url = f"{self.mass.base_url}/stream/{player_id}/{queue_item.queue_item_id}/{stream_job.stream_id}.{fmt}" # noqa: E501 return url - async def get_preview_url(self, provider: str, track_id: str) -> str: + def get_preview_url(self, provider_domain_or_instance_id: str, track_id: str) -> str: """Return url to short preview sample.""" enc_track_id = urllib.parse.quote(track_id) - return f"{self.mass.base_url}/preview?provider={provider}&item_id={enc_track_id}" + return ( + f"{self.mass.base_url}/stream/preview?" + f"provider={provider_domain_or_instance_id}&item_id={enc_track_id}" + ) async def _serve_queue_stream(self, request: web.Request) -> web.Response: """Serve Queue Stream audio to player(s).""" @@ -600,11 +603,11 @@ class StreamsController: async def _serve_preview(self, request: web.Request): """Serve short preview sample.""" - provider_mapping = request.query["provider_mapping"] + provider_domain_or_instance_id = request.query["provider"] item_id = urllib.parse.unquote(request.query["item_id"]) resp = web.StreamResponse(status=200, reason="OK", headers={"Content-Type": "audio/mp3"}) await resp.prepare(request) - async for chunk in get_preview_stream(self.mass, provider_mapping, item_id): + async for chunk in get_preview_stream(self.mass, provider_domain_or_instance_id, item_id): await resp.write(chunk) return resp diff --git a/music_assistant/server/helpers/audio.py b/music_assistant/server/helpers/audio.py index 4ee853e0..2db98933 100644 --- a/music_assistant/server/helpers/audio.py +++ b/music_assistant/server/helpers/audio.py @@ -616,11 +616,11 @@ async def check_audio_support() -> tuple[bool, bool, str]: async def get_preview_stream( mass: MusicAssistant, - provider_mapping: str, + provider_domain_or_instance_id: str, track_id: str, ) -> AsyncGenerator[bytes, None]: """Create a 30 seconds preview audioclip for the given streamdetails.""" - music_prov = mass.get_provider(provider_mapping) + music_prov = mass.get_provider(provider_domain_or_instance_id) streamdetails = await music_prov.get_stream_details(track_id) diff --git a/music_assistant/server/models/provider.py b/music_assistant/server/models/provider.py index 41ee6743..49063a63 100644 --- a/music_assistant/server/models/provider.py +++ b/music_assistant/server/models/provider.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING from music_assistant.common.models.config_entries import ConfigEntryValue, ProviderConfig from music_assistant.common.models.enums import ProviderFeature, ProviderType from music_assistant.common.models.provider import ProviderInstance, ProviderManifest -from music_assistant.constants import ROOT_LOGGER_NAME +from music_assistant.constants import CONF_LOG_LEVEL, ROOT_LOGGER_NAME if TYPE_CHECKING: from music_assistant.server import MusicAssistant @@ -25,7 +25,10 @@ class Provider: self.mass = mass self.manifest = manifest self.config = config - self.logger = logging.getLogger(f"{ROOT_LOGGER_NAME}.providers.{self.domain}") + self.logger = logging.getLogger(f"{ROOT_LOGGER_NAME}.providers.{self.instance_id}") + log_level = config.get_value(CONF_LOG_LEVEL) + if log_level != "GLOBAL": + self.logger.setLevel(log_level) self.cache = mass.cache self.available = False diff --git a/music_assistant/server/providers/chromecast/__init__.py b/music_assistant/server/providers/chromecast/__init__.py index 6bc85ffc..4872c95d 100644 --- a/music_assistant/server/providers/chromecast/__init__.py +++ b/music_assistant/server/providers/chromecast/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +import contextlib import logging import threading import time @@ -93,8 +94,18 @@ class ChromecastProvider(PlayerProvider): """Handle close/cleanup of the provider.""" if not self.browser: return + # stop discovery - await self.mass.loop.run_in_executor(None, self.browser.stop_discovery) + def stop_discovery(): + """Stop the chromecast discovery threads.""" + if self.browser._zc_browser: + with contextlib.suppress(RuntimeError): + self.browser._zc_browser.cancel() + + self.browser.host_browser.stop.set() + self.browser.host_browser.join() + + await self.mass.loop.run_in_executor(None, stop_discovery) # stop all chromecasts for castplayer in list(self.castplayers.values()): await self._disconnect_chromecast(castplayer) @@ -279,7 +290,9 @@ class ChromecastProvider(PlayerProvider): castplayer.mz_controller = mz_controller castplayer.cc.start() - self.mass.loop.call_soon_threadsafe(self.mass.players.register, castplayer.player) + self.mass.loop.call_soon_threadsafe( + self.mass.players.register_or_update, castplayer.player + ) # if player was already added, the player will take care of reconnects itself. castplayer.cast_info.update(disc_info) diff --git a/music_assistant/server/providers/filesystem_local/base.py b/music_assistant/server/providers/filesystem_local/base.py index beea8a3c..09eca0ff 100644 --- a/music_assistant/server/providers/filesystem_local/base.py +++ b/music_assistant/server/providers/filesystem_local/base.py @@ -598,7 +598,8 @@ class FileSystemProviderBase(MusicProvider): """Create a new playlist on provider with given name.""" # creating a new playlist on the filesystem is as easy # as creating a new (empty) file with the m3u extension... - filename = await self.resolve(f"{name}.m3u") + # filename = await self.resolve(f"{name}.m3u") + filename = f"{name}.m3u" await self.write_file_content(filename, b"") playlist = await self.get_playlist(filename) db_playlist = await self.mass.music.playlists.add_db_item(playlist) diff --git a/music_assistant/server/providers/frontend/manifest.json b/music_assistant/server/providers/frontend/manifest.json index a97194fb..16853e6d 100644 --- a/music_assistant/server/providers/frontend/manifest.json +++ b/music_assistant/server/providers/frontend/manifest.json @@ -7,7 +7,7 @@ "config_entries": [ ], - "requirements": ["music-assistant-frontend==20230317.0"], + "requirements": ["music-assistant-frontend==20230319.0"], "documentation": "", "multi_instance": false, "builtin": true, diff --git a/music_assistant/server/providers/json_rpc/manifest.json b/music_assistant/server/providers/json_rpc/manifest.json deleted file mode 100644 index c2350bfb..00000000 --- a/music_assistant/server/providers/json_rpc/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "type": "plugin", - "domain": "json_rpc", - "name": "JSON-RPC API", - "description": "Basic JSON-RPC API implementation, (partly) compatible with Logitech Media Server.", - "codeowners": ["@marcelveldt"], - "config_entries": [ - ], - - "requirements": [], - "documentation": "", - "multi_instance": false, - "builtin": true, - "load_by_default": true -} diff --git a/music_assistant/server/providers/json_rpc/__init__.py b/music_assistant/server/providers/lms_cli/__init__.py similarity index 70% rename from music_assistant/server/providers/json_rpc/__init__.py rename to music_assistant/server/providers/lms_cli/__init__.py index e74072fb..c09d1e75 100644 --- a/music_assistant/server/providers/json_rpc/__init__.py +++ b/music_assistant/server/providers/lms_cli/__init__.py @@ -1,11 +1,14 @@ """JSON-RPC API which is more or less compatible with Logitech Media Server.""" from __future__ import annotations +import asyncio +import urllib.parse from typing import Any from aiohttp import web from music_assistant.common.helpers.json import json_dumps, json_loads +from music_assistant.common.helpers.util import select_free_port from music_assistant.common.models.enums import PlayerState from music_assistant.server.models.plugin import PluginProvider @@ -56,13 +59,86 @@ def parse_args(raw_values: list[int | str]) -> tuple[ArgsType, KwargsType]: return (args, kwargs) -class JSONRPCApi(PluginProvider): - """Basic JSON-RPC API implementation, (partly) compatible with Logitech Media Server.""" +class LmsCli(PluginProvider): + """Basic LMS CLI (json rpc and telnet) implementation, (partly) compatible with Logitech Media Server.""" + + cli_port: int = 9090 async def setup(self) -> None: """Handle async initialization of the plugin.""" + self.logger.info("Registering jsonrpc endpoints on the webserver") self.mass.webapp.router.add_get("/jsonrpc.js", self._handle_jsonrpc) self.mass.webapp.router.add_post("/jsonrpc.js", self._handle_jsonrpc) + # setup (telnet) cli for players requesting basic info on that port + self.cli_port = await select_free_port(9090, 9190) + self.logger.info("Starting (telnet) CLI on port %s", self.cli_port) + await asyncio.start_server(self._handle_cli_client, "0.0.0.0", self.cli_port), + + async def _handle_cli_client( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: + """Handle new connection on the legacy CLI.""" + # https://raw.githubusercontent.com/Logitech/slimserver/public/7.8/HTML/EN/html/docs/cli-api.html + # https://github.com/elParaguayo/LMS-CLI-Documentation/blob/master/LMS-CLI.md + self.logger.info("Client connected on Telnet CLI") + try: + while True: + raw_request = await reader.readline() + raw_request = raw_request.strip().decode("utf-8") + # request comes in as url encoded strings, separated by space + raw_params = [urllib.parse.unquote(x) for x in raw_request.split(" ")] + # the first param is either a macaddress or a command + if ":" in raw_params[0]: + # assume this is a mac address (=player_id) + player_id = raw_params[0] + command = raw_params[1] + command_params = raw_params[2:] + else: + player_id = "" + command = raw_params[0] + command_params = raw_params[1:] + + args, kwargs = parse_args(command_params) + + response: str = raw_request + + # check if we have a handler for this command + # note that we only have support for very limited commands + # just enough for compatibility with players but not to be used as api + # with 3rd party tools! + if handler := getattr(self, f"_handle_{command}", None): + self.logger.debug( + "Handling CLI-request (player: %s command: %s - args: %s - kwargs: %s)", + player_id, + command, + str(args), + str(kwargs), + ) + cmd_result: list[str] = handler(player_id, *args, **kwargs) + if isinstance(cmd_result, dict): + result_parts = dict_to_strings(cmd_result) + result_str = " ".join(urllib.parse.quote(x) for x in result_parts) + elif not cmd_result: + result_str = "" + else: + result_str = str(cmd_result) + response += " " + result_str + else: + self.logger.warning( + "No handler for %s (player: %s - args: %s - kwargs: %s)", + command, + player_id, + str(args), + str(kwargs), + ) + # echo back the request and the result (if any) + response += "\n" + writer.write(response.encode("utf-8")) + await writer.drain() + except Exception as err: + self.logger.debug("Error handling CLI command", exc_info=err) + finally: + self.logger.debug("Client disconnected from Telnet CLI") async def _handle_jsonrpc(self, request: web.Request) -> web.Response: """Handle request for image proxy.""" @@ -283,3 +359,23 @@ class JSONRPCApi(PluginProvider): self.mass.create_task(self.mass.players.queues.pause, player_id) else: self.mass.create_task(self.mass.players.queues.play, player_id) + + +def dict_to_strings(source: dict) -> list[str]: + """Convert dict to key:value strings (used in slimproto cli).""" + result: list[str] = [] + + for key, value in source.items(): + if value in (None, ""): + continue + if isinstance(value, list): + for subval in value: + if isinstance(subval, dict): + result += dict_to_strings(subval) + else: + result.append(str(subval)) + elif isinstance(value, dict): + result += dict_to_strings(subval) + else: + result.append(f"{key}:{str(value)}") + return result diff --git a/music_assistant/server/providers/lms_cli/manifest.json b/music_assistant/server/providers/lms_cli/manifest.json new file mode 100644 index 00000000..dee5fec2 --- /dev/null +++ b/music_assistant/server/providers/lms_cli/manifest.json @@ -0,0 +1,14 @@ +{ + "type": "plugin", + "domain": "lms_cli", + "name": "LMS CLI", + "description": "Basic CLI implementation (classic + JSON-RPC), which is (partly) compatible with Logitech Media Server to maximize compatibility with Squeezebox players.", + "codeowners": ["@marcelveldt"], + "config_entries": [ + ], + "requirements": [], + "documentation": "", + "multi_instance": false, + "builtin": true, + "load_by_default": true +} diff --git a/music_assistant/server/providers/json_rpc/models.py b/music_assistant/server/providers/lms_cli/models.py similarity index 100% rename from music_assistant/server/providers/json_rpc/models.py rename to music_assistant/server/providers/lms_cli/models.py diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index a969bcd2..59f32c89 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio import time -import urllib.parse from collections import deque from collections.abc import Callable, Generator from dataclasses import dataclass @@ -15,7 +14,6 @@ from aioslimproto.client import TransitionType as SlimTransition from aioslimproto.const import EventType as SlimEventType from aioslimproto.discovery import start_discovery -from music_assistant.common.helpers.util import select_free_port from music_assistant.common.models.config_entries import ConfigEntry from music_assistant.common.models.enums import ( ConfigEntryType, @@ -29,10 +27,9 @@ from music_assistant.common.models.player import DeviceInfo, Player from music_assistant.common.models.queue_item import QueueItem from music_assistant.constants import CONF_PLAYERS from music_assistant.server.models.player_provider import PlayerProvider -from music_assistant.server.providers.json_rpc import parse_args if TYPE_CHECKING: - from music_assistant.server.providers.json_rpc import JSONRPCApi + pass # sync constants MIN_DEVIATION_ADJUST = 10 # 10 milliseconds @@ -100,15 +97,13 @@ class SlimprotoProvider(PlayerProvider): # autodiscovery of the slimproto server does not work # when the port is not the default (3483) so we hardcode it for now slimproto_port = 3483 - cli_port = await select_free_port(9090, 9190) + cli_port = cli_prov.cli_port if (cli_prov := self.mass.get_provider("lms_cli")) else None self.logger.info("Starting SLIMProto server on port %s", slimproto_port) self._socket_servers = ( # start slimproto server await asyncio.start_server(self._create_client, "0.0.0.0", slimproto_port), # setup discovery await start_discovery(slimproto_port, cli_port, self.mass.port), - # setup (telnet) cli for players requesting basic info on that port - await asyncio.start_server(self._handle_cli_client, "0.0.0.0", cli_port), ) async def close(self) -> None: @@ -366,7 +361,7 @@ class SlimprotoProvider(PlayerProvider): if virtual_provider_info: # if this player is part of a virtual provider run the callback virtual_provider_info[0](player) - self.mass.players.register(player) + self.mass.players.register_or_update(player) # update player state on player events player.available = True @@ -540,90 +535,3 @@ class SlimprotoProvider(PlayerProvider): if sync_delay != 0: return client.elapsed_milliseconds - sync_delay return client.elapsed_milliseconds - - async def _handle_cli_client( - self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter - ) -> None: - """Handle new connection on the legacy CLI.""" - # https://raw.githubusercontent.com/Logitech/slimserver/public/7.8/HTML/EN/html/docs/cli-api.html - self.logger.info("Client connected on Telnet CLI") - try: - while True: - raw_request = await reader.readline() - raw_request = raw_request.strip().decode("utf-8") - # request comes in as url encoded strings, separated by space - raw_params = [urllib.parse.unquote(x) for x in raw_request.split(" ")] - # the first param is either a macaddress or a command - if ":" in raw_params[0]: - # assume this is a mac address (=player_id) - player_id = raw_params[0] - command = raw_params[1] - command_params = raw_params[2:] - else: - player_id = "" - command = raw_params[0] - command_params = raw_params[1:] - - args, kwargs = parse_args(command_params) - - response: str = raw_request - - # check if we have a handler for this command - # note that we only have support for very limited commands - # just enough for compatibility with players but not to be used as api - # with 3rd party tools! - json_rpc: JSONRPCApi = self.mass.get_provider("json_rpc") - assert json_rpc is not None - if handler := getattr(json_rpc, f"_handle_{command}", None): - self.logger.debug( - "Handling CLI-request (player: %s command: %s - args: %s - kwargs: %s)", - player_id, - command, - str(args), - str(kwargs), - ) - cmd_result: list[str] = handler(player_id, *args, **kwargs) - if isinstance(cmd_result, dict): - result_parts = dict_to_strings(cmd_result) - result_str = " ".join(urllib.parse.quote(x) for x in result_parts) - elif not cmd_result: - result_str = "" - else: - result_str = str(cmd_result) - response += " " + result_str - else: - self.logger.warning( - "No handler for %s (player: %s - args: %s - kwargs: %s)", - command, - player_id, - str(args), - str(kwargs), - ) - # echo back the request and the result (if any) - response += "\n" - writer.write(response.encode("utf-8")) - await writer.drain() - except Exception as err: - self.logger.debug("Error handling CLI command", exc_info=err) - finally: - self.logger.debug("Client disconnected from Telnet CLI") - - -def dict_to_strings(source: dict) -> list[str]: - """Convert dict to key:value strings (used in slimproto cli).""" - result: list[str] = [] - - for key, value in source.items(): - if value in (None, ""): - continue - if isinstance(value, list): - for subval in value: - if isinstance(subval, dict): - result += dict_to_strings(subval) - else: - result.append(str(subval)) - elif isinstance(value, dict): - result += dict_to_strings(subval) - else: - result.append(f"{key}:{str(value)}") - return result diff --git a/music_assistant/server/providers/slimproto/manifest.json b/music_assistant/server/providers/slimproto/manifest.json index 7c8d7dde..3955d8b7 100644 --- a/music_assistant/server/providers/slimproto/manifest.json +++ b/music_assistant/server/providers/slimproto/manifest.json @@ -9,6 +9,7 @@ "requirements": ["aioslimproto==2.2.0"], "documentation": "https://github.com/music-assistant/hass-music-assistant/discussions/1123", "multi_instance": false, - "builtin": true, - "load_by_default": true + "builtin": false, + "load_by_default": true, + "depends_on": "lms_cli" } diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py index 73dcf60e..066bc2c4 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/server/providers/sonos/__init__.py @@ -446,7 +446,7 @@ class SonosPlayerProvider(PlayerProvider): self.sonosplayers[player_id] = sonos_player - self.mass.players.register(sonos_player.player) + self.mass.players.register_or_update(sonos_player.player) def _handle_av_transport_event(self, sonos_player: SonosPlayer, event: SonosEvent): """Handle a soco.SoCo AVTransport event.""" diff --git a/requirements_all.txt b/requirements_all.txt index 0cf44d48..e4d02e21 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -14,7 +14,7 @@ getmac==0.8.2 git+https://github.com/pytube/pytube.git@refs/pull/1501/head mashumaro==3.5.0 memory-tempfile==2.2.3 -music-assistant-frontend==20230317.0 +music-assistant-frontend==20230319.0 orjson==3.8.7 pillow==9.4.0 PyChromecast==13.0.4 -- 2.34.1