diff --git a/custom_components/mediabrowser/__init__.py b/custom_components/mediabrowser/__init__.py index f2eae2a..0a19d46 100644 --- a/custom_components/mediabrowser/__init__.py +++ b/custom_components/mediabrowser/__init__.py @@ -3,7 +3,6 @@ import asyncio import logging -from typing import Any import aiohttp from homeassistant.config_entries import ( @@ -12,9 +11,9 @@ ConfigEntryNotReady, ) from homeassistant.const import CONF_URL, Platform -from homeassistant.core import HomeAssistant, Context +from homeassistant.core import HomeAssistant -from .helpers import snake_cased_json +from .helpers import size_of, snake_cased_json from .const import ( CONF_CACHE_SERVER_API_KEY, @@ -23,12 +22,7 @@ CONF_CACHE_SERVER_PING, CONF_CACHE_SERVER_USER_ID, CONF_CACHE_SERVER_VERSION, - CONF_SENSOR_ITEM_TYPE, - CONF_SENSOR_LIBRARY, - CONF_SENSOR_USER, - CONF_SENSORS, DATA_HUB, - DATA_POLL_COORDINATOR, DOMAIN, ) from .hub import MediaBrowserHub @@ -46,7 +40,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_websocket_message( message_type: str, data: dict[str, None] | None ) -> None: - _LOGGER.debug("%s firing event %s_%s", hub.server_name, DOMAIN, message_type) + _LOGGER.debug( + "%s firing event %s_%s (%d bytes)", + hub.server_name, + DOMAIN, + message_type, + size_of(data), + ) hass.bus.async_fire( f"{DOMAIN}_{message_type}", snake_cased_json(data), diff --git a/custom_components/mediabrowser/const.py b/custom_components/mediabrowser/const.py index 0f918da..5ef4fa2 100644 --- a/custom_components/mediabrowser/const.py +++ b/custom_components/mediabrowser/const.py @@ -204,6 +204,7 @@ class Item(StrEnum): MBCLIENT = "MediaBrowserClient" MEDIA_SOURCE_ID = "MediaSourceId" MEDIA_SOURCES = "MediaSources" + MEDIA_STREAMS = "MediaStreams" MEDIA_TYPE = "MediaType" MESSAGE_TYPE = "MessageType" NAME = "Name" @@ -362,6 +363,36 @@ class VirtualFolder(StrEnum): YEARS = "years" +class UserDataChange(StrEnum): + """Keys for UserDataChanged event""" + + USER_ID = "UserId" + USER_DATA_LIST = "UserDataList" + + +class LibraryChange(StrEnum): + """Keys for LibraryChange event""" + + ITEMS_ADDED = "ItemsAdded" + ITEMS_UPDATED = "ItemsUpdated" + ITEMS_REMOVED = "ItemsRemoved" + FOLDERS_ADDED_TO = "FoldersAddedTo" + FOLDERS_REMOVED_FROM = "FoldersRemovedFrom" + COLLECTION_FOLDERS = "CollectionFolders" + + +class WebsocketMessage(StrEnum): + """Websocket message types""" + + ACTIVITY_LOG_ENTRY = "ActivityLogEntry" + FORCE_KEEP_ALIVE = "ForceKeepAlive" + KEEP_ALIVE = "KeepAlive" + LIBRARY_CHANGED = "LibraryChanged" + SCHEDULED_TASK_INFO = "ScheduledTaskInfo" + SESSIONS = "Sessions" + USER_DATA_CHANGED = "UserDataChanged" + + class ImageType(StrEnum): """Image types.""" @@ -454,6 +485,7 @@ class Session(StrEnum): PLAYLIST_INDEX = "PlaylistIndex" PLAYLIST_LENGTH = "PlaylistLength" REMOTE_END_POINT = "RemoteEndPoint" + SERVER_ID = "ServerId" SUPPORTS_REMOTE_CONTROL = "SupportsRemoteControl" SUPPORTED_COMMANDS = "SupportedCommands" USER_NAME = "UserName" diff --git a/custom_components/mediabrowser/helpers.py b/custom_components/mediabrowser/helpers.py index ccf0fe4..6ab42f6 100644 --- a/custom_components/mediabrowser/helpers.py +++ b/custom_components/mediabrowser/helpers.py @@ -4,6 +4,7 @@ from datetime import datetime import logging import re +from sys import getsizeof from typing import Any from dateutil import parser @@ -18,6 +19,9 @@ ImageType, ItemType, Item, + LibraryChange, + Session, + UserDataChange, ) _LOGGER = logging.getLogger(__package__) @@ -213,8 +217,90 @@ def camel_cased_json(original: Any | None) -> Any | None: def autolog(message): - "Automatically log the current function details." + """Automatically log the current function details.""" func = inspect.currentframe().f_back.f_code # type: ignore _LOGGER.debug( "%s: %s in %s:%i", message, func.co_name, func.co_filename, func.co_firstlineno ) + + +def get_session_event_data(session: dict[str, Any]) -> dict[str, Any]: + """Translate session information in event data""" + result: dict[str, Any] = {} + + for key in [ + Session.REMOTE_END_POINT, + Session.ID, + Session.CLIENT, + Session.LAST_ACTIVITY_DATE, + Session.SERVER_ID, + Session.DEVICE_NAME, + Session.APPLICATION_VERSION, + Session.PLAY_STATE, + Session.APP_ICON_URL, + Session.SUPPORTS_REMOTE_CONTROL, + ]: + if field := session.get(key): + result[key] = field + + if play_state := session.get(Session.PLAY_STATE): + result |= play_state + + if npi := session.get(Session.NOW_PLAYING_ITEM): + for key in [ + Item.NAME, + Item.ID, + Item.PARENT_ID, + Item.PATH, + Item.RUNTIME_TICKS, + Item.TYPE, + Item.MEDIA_TYPE, + ]: + if field := npi.get(key): + result[f"NowPlaying{key}"] = field + return result + + +def get_user_data_changed_event_data(event: dict[str, Any]) -> dict[str, Any]: + """Translate user data changed notification in event data""" + result = {} + if user_id := event.get(UserDataChange.USER_ID): + result[UserDataChange.USER_ID] = user_id + if data_list := event.get(UserDataChange.USER_DATA_LIST): + if len(data_list) > 5: + result[UserDataChange.USER_DATA_LIST] = data_list[:5] + else: + result[UserDataChange.USER_DATA_LIST] = data_list + + return result + + +def get_library_changed_event_data(event: dict[str, Any]) -> dict[str, Any]: + """Translate user data changed notification in event data""" + result = {} + for key in [ + LibraryChange.FOLDERS_ADDED_TO, + LibraryChange.ITEMS_ADDED, + LibraryChange.ITEMS_REMOVED, + LibraryChange.ITEMS_UPDATED, + LibraryChange.FOLDERS_REMOVED_FROM, + ]: + if data := event.get(key): + if len(data) > 5: + result[key] = data[:5] + else: + result[key] = data + + return result + + +def size_of(data: Any): + """Returns the approximate memory footprint an object and all of its contents.""" + + result = getsizeof(data) + if isinstance(data, list): + result += sum((size_of(item) for item in data)) + elif isinstance(data, dict): + result += sum((size_of(key) + size_of(item) for key, item in data.items())) + + return result diff --git a/custom_components/mediabrowser/hub.py b/custom_components/mediabrowser/hub.py index bea1734..9426c46 100644 --- a/custom_components/mediabrowser/hub.py +++ b/custom_components/mediabrowser/hub.py @@ -14,7 +14,12 @@ from homeassistant.const import CONF_USERNAME, CONF_NAME, CONF_PASSWORD, CONF_URL from homeassistant.util import uuid -from .helpers import snake_case +from .helpers import ( + get_library_changed_event_data, + get_user_data_changed_event_data, + get_session_event_data, + snake_case, +) from .const import ( APP_PLAYERS, @@ -65,6 +70,7 @@ ServerType, Session, Value, + WebsocketMessage, ) @@ -786,7 +792,10 @@ async def _handle_sessions_message(self, sessions: list[dict[str, Any]]) -> None old_raw_sessions = deepcopy(self._raw_sessions) old_sessions = deepcopy(self._sessions) - new_raw_sessions = {session["Id"]: session for session in sessions} + new_raw_sessions = { + session["Id"]: get_session_event_data(deepcopy(session)) + for session in sessions + } new_sessions = { session["Id"]: session for session in self._preprocess_sessions(sessions) } @@ -918,39 +927,42 @@ async def _handle_message(self, message: str): if msg_type := msg.get("MessageType"): call_listeners = self.send_other_events + data = msg.get("Data") match msg_type: - case "Sessions": - sessions = deepcopy(msg["Data"]) + case WebsocketMessage.SESSIONS: + sessions = deepcopy(data) asyncio.ensure_future(self._handle_sessions_message(sessions)) call_listeners = False - case "KeepAlive": + case WebsocketMessage.KEEP_ALIVE: _LOGGER.debug( "KeepAlive response received from %s", self.server_url ) call_listeners = False - case "ForceKeepAlive": + case WebsocketMessage.FORCE_KEEP_ALIVE: _LOGGER.debug( "ForceKeepAlive response received from %s", self.server_url ) self._keep_alive_timeout = msg.get("Data", KEEP_ALIVE_TIMEOUT) / 2 call_listeners = False - case "LibraryChanged": - event = msg["Data"] + case WebsocketMessage.LIBRARY_CHANGED: if any(self._library_listeners): asyncio.ensure_future( - self._handle_library_changed_message(event) + self._handle_library_changed_message(data) ) - case "ActivityLogEntry": + data = get_library_changed_event_data(data) + case WebsocketMessage.ACTIVITY_LOG_ENTRY: if self.send_activity_events and any(self._websocket_listeners): asyncio.ensure_future(self._handle_activity_log_message()) call_listeners = False - case "ScheduledTasksInfo": + case WebsocketMessage.SCHEDULED_TASK_INFO: call_listeners = self.send_task_events + case WebsocketMessage.USER_DATA_CHANGED: + data = get_user_data_changed_event_data(data) if call_listeners and any(self._websocket_listeners): data = { "server_id": self.server_id, - snake_case(msg_type): deepcopy(msg.get("Data", {})), + snake_case(msg_type): (data or {}), } asyncio.ensure_future( self._call_websocket_listeners(snake_case(msg_type), data)