From: Marcel van der Veldt Date: Fri, 24 Jan 2025 11:22:31 +0000 (+0100) Subject: Feat: Implement (very basic) json RPC api X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=927ccf1e8ce6002594295dff453570c1064a0edd;p=music-assistant-server.git Feat: Implement (very basic) json RPC api Ability to issue commands on the api directly using json post --- diff --git a/music_assistant/controllers/webserver.py b/music_assistant/controllers/webserver.py index 492573f6..8f8a5427 100644 --- a/music_assistant/controllers/webserver.py +++ b/music_assistant/controllers/webserver.py @@ -31,6 +31,7 @@ from music_assistant_models.errors import InvalidCommand from music_assistant.constants import CONF_BIND_IP, CONF_BIND_PORT, VERBOSE_LOG_LEVEL from music_assistant.helpers.api import APICommandHandler, parse_arguments from music_assistant.helpers.audio import get_preview_stream +from music_assistant.helpers.json import json_dumps from music_assistant.helpers.util import get_ip, get_ips from music_assistant.helpers.webserver import Webserver from music_assistant.models.core_controller import CoreController @@ -159,6 +160,8 @@ class WebserverController(CoreController): routes.append(("GET", "/imageproxy", self.mass.metadata.handle_imageproxy)) # also host the audio preview service routes.append(("GET", "/preview", self.serve_preview_stream)) + # add jsonrpc api + routes.append(("POST", "/api", self._handle_jsonrpc_api_command)) # start the webserver default_publish_ip = await get_ip() if self.mass.running_as_hass_addon: @@ -221,6 +224,34 @@ class WebserverController(CoreController): finally: self.clients.remove(connection) + async def _handle_jsonrpc_api_command(self, request: web.Request) -> web.Response: + """Handle incoming JSON RPC API command.""" + if not request.can_read_body: + return web.Response(status=400, text="Body required") + cmd_data = await request.read() + self.logger.log(VERBOSE_LOG_LEVEL, "Received on JSONRPC API: %s", cmd_data) + try: + command_msg = CommandMessage.from_json(cmd_data) + except ValueError: + error = f"Invalid JSON: {cmd_data}" + self.logger.error("Unhandled JSONRPC API error: %s", error) + return web.Response(status=400, text=error) + + # work out handler for the given path/command + handler = self.mass.command_handlers.get(command_msg.command) + if handler is None: + error = f"Invalid Command: {command_msg.command}" + self.logger.error("Unhandled JSONRPC API error: %s", error) + return web.Response(status=400, text=error) + args = parse_arguments(handler.signature, handler.type_hints, command_msg.args) + result = handler.target(**args) + if hasattr(result, "__anext__"): + # handle async generator (for really large listings) + result = [item async for item in result] + elif asyncio.iscoroutine(result): + result = await result + return web.json_response(result, dumps=json_dumps) + async def _handle_application_log(self, request: web.Request) -> web.Response: """Handle request to get the application log.""" log_data = await self.mass.get_application_log()