diff --git a/homeassistant/components/jellyfin/browse_media.py b/homeassistant/components/jellyfin/browse_media.py index 9eee4bbb363..9dc84971a21 100644 --- a/homeassistant/components/jellyfin/browse_media.py +++ b/homeassistant/components/jellyfin/browse_media.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from functools import partial from typing import Any from jellyfin_apiclient_python import JellyfinClient @@ -12,6 +13,7 @@ from homeassistant.components.media_player import ( BrowseMedia, MediaClass, MediaType, + SearchMediaQuery, ) from homeassistant.core import HomeAssistant @@ -156,6 +158,51 @@ def fetch_items( ] +async def search_items( + hass: HomeAssistant, client: JellyfinClient, user_id: str, query: SearchMediaQuery +) -> list[BrowseMedia]: + """Search items in Jellyfin server.""" + search_result: list[BrowseMedia] = [] + + items: list[dict[str, Any]] = [] + # Search for items based on media filter classes (or all if none specified) + media_types: list[MediaClass] | list[None] = [] + if query.media_filter_classes: + media_types = query.media_filter_classes + else: + media_types = [None] + + for media_type in media_types: + items_dict: dict[str, Any] = await hass.async_add_executor_job( + partial( + client.jellyfin.search_media_items, + term=query.search_query, + media=media_type, + parent_id=query.media_content_id, + ) + ) + items.extend(items_dict.get("Items", [])) + + for item in items: + content_type: str = item["MediaType"] + + response = BrowseMedia( + media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get( + content_type, MediaClass.DIRECTORY + ), + media_content_id=item["Id"], + media_content_type=content_type, + title=item["Name"], + thumbnail=get_artwork_url(client, item), + can_play=bool(content_type in PLAYABLE_MEDIA_TYPES), + can_expand=item.get("IsFolder", False), + children=None, + ) + search_result.append(response) + + return search_result + + async def get_media_info( hass: HomeAssistant, client: JellyfinClient, diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py index b71c0bf93c9..6f3c41d282f 100644 --- a/homeassistant/components/jellyfin/media_player.py +++ b/homeassistant/components/jellyfin/media_player.py @@ -11,12 +11,14 @@ from homeassistant.components.media_player import ( MediaPlayerEntityFeature, MediaPlayerState, MediaType, + SearchMedia, + SearchMediaQuery, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.dt import parse_datetime -from .browse_media import build_item_response, build_root_response +from .browse_media import build_item_response, build_root_response, search_items from .client_wrapper import get_artwork_url from .const import CONTENT_TYPE_MAP, LOGGER, MAX_IMAGE_WIDTH from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator @@ -196,6 +198,7 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity): | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.SEEK + | MediaPlayerEntityFeature.SEARCH_MEDIA ) if "Mute" in commands: @@ -274,3 +277,13 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity): media_content_type, media_content_id, ) + + async def async_search_media( + self, + query: SearchMediaQuery, + ) -> SearchMedia: + """Search the media player.""" + result = await search_items( + self.hass, self.coordinator.api_client, self.coordinator.user_id, query + ) + return SearchMedia(result=result) diff --git a/tests/components/jellyfin/conftest.py b/tests/components/jellyfin/conftest.py index c3732714177..71088dea2ea 100644 --- a/tests/components/jellyfin/conftest.py +++ b/tests/components/jellyfin/conftest.py @@ -81,6 +81,7 @@ def mock_api() -> MagicMock: jf_api.get_item.side_effect = api_get_item_side_effect jf_api.get_media_folders.return_value = load_json_fixture("get-media-folders.json") jf_api.user_items.side_effect = api_user_items_side_effect + jf_api.search_media_items.return_value = load_json_fixture("user-items.json") return jf_api diff --git a/tests/components/jellyfin/test_media_player.py b/tests/components/jellyfin/test_media_player.py index 404fdc801ee..b4506f5a607 100644 --- a/tests/components/jellyfin/test_media_player.py +++ b/tests/components/jellyfin/test_media_player.py @@ -363,6 +363,47 @@ async def test_browse_media( ) +async def test_search_media( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + init_integration: MockConfigEntry, + mock_jellyfin: MagicMock, + mock_api: MagicMock, +) -> None: + """Test Jellyfin browse media.""" + client = await hass_ws_client() + + # browse root folder + await client.send_json( + { + "id": 1, + "type": "media_player/search_media", + "entity_id": "media_player.jellyfin_device", + "media_content_id": "", + "media_content_type": "", + "search_query": "Fake Item 1", + "media_filter_classes": ["movie"], + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"]["result"] == [ + { + "title": "FOLDER", + "media_class": MediaClass.DIRECTORY.value, + "media_content_type": "string", + "media_content_id": "FOLDER-UUID", + "children_media_class": None, + "can_play": False, + "can_expand": True, + "can_search": False, + "not_shown": 0, + "thumbnail": "http://localhost/Items/21af9851-8e39-43a9-9c47-513d3b9e99fc/Images/Primary.jpg", + "children": [], + } + ] + + async def test_new_client_connected( hass: HomeAssistant, init_integration: MockConfigEntry,