"""Several generic/basic helpers and utilities complementing the Music Assistant Client.""" from __future__ import annotations import asyncio import base64 from _collections_abc import dict_keys, dict_values from types import MethodType from typing import TYPE_CHECKING, Any import orjson if TYPE_CHECKING: from music_assistant_models.media_items import SearchResults JSON_ENCODE_EXCEPTIONS = (TypeError, ValueError) JSON_DECODE_EXCEPTIONS = (orjson.JSONDecodeError,) DO_NOT_SERIALIZE_TYPES = (MethodType, asyncio.Task) def compact_media_item_dict(item: dict[str, Any]) -> dict[str, Any]: """Return compacted MediaItem dict.""" for key in ( "metadata", "provider_mappings", "favorite", "timestamp_added", "timestamp_modified", "mbid", ): item.pop(key, None) for key, value in item.items(): if isinstance(value, dict): item[key] = compact_media_item_dict(value) elif isinstance(value, list): for subitem in value: if not isinstance(subitem, dict): continue compact_media_item_dict(subitem) return item def searchresults_as_compact_dict(search_results: SearchResults) -> dict[str, Any]: """Return compacted search result dict.""" dict_result: dict[str, list[dict[str, Any]]] = search_results.to_dict() for media_type_key in dict_result: # noqa: PLC0206 for item in dict_result[media_type_key]: if not isinstance(item, dict): # guards against invalid data continue # type: ignore[unreachable] # return limited result to prevent it being too verbose compact_media_item_dict(item) return dict_result def get_serializable_value(obj: Any, raise_unhandled: bool = False) -> Any: """Parse the value to its serializable equivalent.""" if getattr(obj, "do_not_serialize", None): return None if ( isinstance(obj, list | set | filter | tuple | dict_values | dict_keys | dict_values) or obj.__class__ == "dict_valueiterator" ): return [get_serializable_value(x) for x in obj] if hasattr(obj, "to_dict"): return obj.to_dict() if isinstance(obj, bytes): return base64.b64encode(obj).decode("ascii") if isinstance(obj, DO_NOT_SERIALIZE_TYPES): return None if raise_unhandled: raise TypeError return obj def serialize_to_json(obj: Any) -> Any: """Serialize a value (or a list of values) to json.""" if obj is None: return obj if hasattr(obj, "to_json"): return obj.to_json() return json_dumps(get_serializable_value(obj)) def json_dumps(data: Any, indent: bool = False) -> str: """Dump json string.""" # we use the passthrough dataclass option because we use mashumaro for that option = orjson.OPT_OMIT_MICROSECONDS | orjson.OPT_PASSTHROUGH_DATACLASS if indent: option |= orjson.OPT_INDENT_2 return orjson.dumps( data, default=get_serializable_value, option=option, ).decode("utf-8") json_loads = orjson.loads