Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions ucapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from zeroconf import IPVersion
from zeroconf.asyncio import AsyncServiceInfo, AsyncZeroconf

from . import StatusCodes
from . import api_definitions as uc
from .entities import Entities
from .entity import EntityTypes
Expand Down Expand Up @@ -711,6 +712,10 @@ async def _handle_ws_request_msg(
)
elif msg == uc.WsMessages.ENTITY_COMMAND:
await self._entity_command(websocket, req_id, msg_data)
elif msg == uc.WsMessages.BROWSE_MEDIA:
await self._browse_media(websocket, req_id, msg_data)
elif msg == uc.WsMessages.SEARCH_MEDIA:
await self._search_media(websocket, req_id, msg_data)
elif msg == uc.WsMessages.SUBSCRIBE_EVENTS:
await self._subscribe_events(websocket, msg_data)
await self._send_ok_result(websocket, req_id)
Expand Down Expand Up @@ -922,6 +927,82 @@ async def _entity_command(

await self.acknowledge_command(websocket, req_id, result)

async def _browse_media(
self, websocket, req_id: int, msg_data: dict[str, Any] | None
) -> None:
if not msg_data:
_LOG.warning("Ignoring entity command: called with empty msg_data")
await self.acknowledge_command(
websocket, req_id, uc.StatusCodes.BAD_REQUEST
)
return

entity_id = msg_data["entity_id"] if "entity_id" in msg_data else None
if entity_id is None:
_LOG.warning("Ignoring command: missing entity_id")
await self.acknowledge_command(
websocket, req_id, uc.StatusCodes.BAD_REQUEST
)
return

entity = self.configured_entities.get(entity_id)
if entity is None:
_LOG.warning(
"Cannot browse media for '%s': no configured entity found",
entity_id,
)
await self.acknowledge_command(websocket, req_id, uc.StatusCodes.NOT_FOUND)
return

result = await entity.browse_media(
msg_data,
websocket=websocket,
)
if isinstance(result, dict):
await self._send_ws_response(
websocket, req_id, "media_browse", result, StatusCodes.OK
)
else:
await self.acknowledge_command(websocket, req_id, result)

async def _search_media(
self, websocket, req_id: int, msg_data: dict[str, Any] | None
) -> None:
if not msg_data:
_LOG.warning("Ignoring entity command: called with empty msg_data")
await self.acknowledge_command(
websocket, req_id, uc.StatusCodes.BAD_REQUEST
)
return

entity_id = msg_data["entity_id"] if "entity_id" in msg_data else None
if entity_id is None:
_LOG.warning("Ignoring command: missing entity_id")
await self.acknowledge_command(
websocket, req_id, uc.StatusCodes.BAD_REQUEST
)
return

entity = self.configured_entities.get(entity_id)
if entity is None:
_LOG.warning(
"Cannot search media for '%s': no configured entity found",
entity_id,
)
await self.acknowledge_command(websocket, req_id, uc.StatusCodes.NOT_FOUND)
return

result = await entity.search_media(
msg_data,
websocket=websocket,
)
if isinstance(result, dict):
await self._send_ws_response(
websocket, req_id, "media_search", result, StatusCodes.OK
)
else:
await self.acknowledge_command(websocket, req_id, result)

async def _setup_driver(
self, websocket, req_id: int, msg_data: dict[str, Any] | None
) -> bool:
Expand Down
2 changes: 2 additions & 0 deletions ucapi/api_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class WsMessages(str, Enum):
GET_DRIVER_METADATA = "get_driver_metadata"
SETUP_DRIVER = "setup_driver"
SET_DRIVER_USER_DATA = "set_driver_user_data"
BROWSE_MEDIA = "browse_media"
SEARCH_MEDIA = "search_media"


# Does WsMsgEvents need to be public?
Expand Down
38 changes: 38 additions & 0 deletions ucapi/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,41 @@ async def command(
return await handler(self, cmd_id, params, websocket=websocket)

return await handler(self, cmd_id, params)

# pylint: disable=W0613
async def browse_media(
self,
params: dict[str, Any],
*,
websocket: Any,
) -> dict[str, Any] | StatusCodes:
"""
Execute entity browsing request.

Returns NOT_IMPLEMENTED if no handler is installed.

:param params: browsing parameters
:param websocket: optional websocket connection. Allows for directed event
callbacks instead of broadcasts.
:return: browsing response or status code if any error occurs
"""
return StatusCodes.NOT_IMPLEMENTED

# pylint: disable=W0613
async def search_media(
self,
params: dict[str, Any],
*,
websocket: Any,
) -> dict[str, Any] | StatusCodes:
"""
Execute media search request.

Returns NOT_IMPLEMENTED if no handler is installed.

:param params: search parameters
:param websocket: optional websocket connection. Allows for directed event
callbacks instead of broadcasts.
:return: search response or status code if any error occurs
"""
return StatusCodes.NOT_IMPLEMENTED
Comment on lines +151 to +187
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

browse_media and search_media need to be in the MediaPlayer class.
The IntegrationAPI needs to check if the provided entity ID relates to a media player entity and return "404 not found" if not found or mismatch.

Functionality should be similar to the Node.js implementation:

64 changes: 64 additions & 0 deletions ucapi/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ class Features(str, Enum):
SUBTITLE = "subtitle"
RECORD = "record"
SETTINGS = "settings"
PLAY_MEDIA = "play_media"
CLEAR_PLAYLIST = "clear_playlist"
BROWSE_MEDIA = "browse_media"
SEARCH_MEDIA = "search_media"
SEARCH_MEDIA_CLASSES = "search_media_classes"


class Attributes(str, Enum):
Expand Down Expand Up @@ -151,6 +156,7 @@ class Commands(str, Enum):
SUBTITLE = "subtitle"
SETTINGS = "settings"
SEARCH = "search"
PLAY_MEDIA = "play_media"


class DeviceClasses(str, Enum):
Expand Down Expand Up @@ -188,6 +194,64 @@ class RepeatMode(str, Enum):
ONE = "ONE"


class MediaPlayAction(str, Enum):
"""Media Play actions."""

PLAY_NOW = "PLAY_NOW"
PLAY_NEXT = "PLAY_NEXT"
ADD_TO_QUEUE = "ADD_TO_QUEUE"


class MediaContent(str, Enum):
"""Media content types for media browsing."""

ALBUM = "album"
APP = "app"
APPS = "apps"
ARTIST = "artist"
CHANNEL = "channel"
CHANNELS = "channels"
COMPOSER = "composer"
EPISODE = "episode"
GAME = "game"
GENRE = "genre"
IMAGE = "image"
MOVIE = "movie"
MUSIC = "music"
PLAYLIST = "playlist"
PODCAST = "podcast"
RADIO = "radio"
SEASON = "season"
TRACK = "track"
TV_SHOW = "tv_show"
URL = "url"
VIDEO = "video"


class MediaClass(str, Enum):
"""Media classes for media browsing."""

ALBUM = "album"
APP = "app"
ARTIST = "artist"
CHANNEL = "channel"
COMPOSER = "composer"
DIRECTORY = "directory"
EPISODE = "episode"
GAME = "game"
GENRE = "genre"
IMAGE = "image"
MOVIE = "movie"
MUSIC = "music"
PLAYLIST = "playlist"
PODCAST = "podcast"
SEASON = "season"
TRACK = "track"
TV_SHOW = "tv_show"
URL = "url"
VIDEO = "video"


class MediaPlayer(Entity):
"""
Media-player entity class.
Expand Down
Loading