Add search functionality to jellyfin (#148822)

This commit is contained in:
Josef Zweck 2025-07-16 13:26:46 +02:00 committed by GitHub
parent e28f02d163
commit 26a9af7371
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 103 additions and 1 deletions

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from functools import partial
from typing import Any from typing import Any
from jellyfin_apiclient_python import JellyfinClient from jellyfin_apiclient_python import JellyfinClient
@ -12,6 +13,7 @@ from homeassistant.components.media_player import (
BrowseMedia, BrowseMedia,
MediaClass, MediaClass,
MediaType, MediaType,
SearchMediaQuery,
) )
from homeassistant.core import HomeAssistant 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( async def get_media_info(
hass: HomeAssistant, hass: HomeAssistant,
client: JellyfinClient, client: JellyfinClient,

View File

@ -11,12 +11,14 @@ from homeassistant.components.media_player import (
MediaPlayerEntityFeature, MediaPlayerEntityFeature,
MediaPlayerState, MediaPlayerState,
MediaType, MediaType,
SearchMedia,
SearchMediaQuery,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.dt import parse_datetime 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 .client_wrapper import get_artwork_url
from .const import CONTENT_TYPE_MAP, LOGGER, MAX_IMAGE_WIDTH from .const import CONTENT_TYPE_MAP, LOGGER, MAX_IMAGE_WIDTH
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
@ -196,6 +198,7 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
| MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.STOP | MediaPlayerEntityFeature.STOP
| MediaPlayerEntityFeature.SEEK | MediaPlayerEntityFeature.SEEK
| MediaPlayerEntityFeature.SEARCH_MEDIA
) )
if "Mute" in commands: if "Mute" in commands:
@ -274,3 +277,13 @@ class JellyfinMediaPlayer(JellyfinClientEntity, MediaPlayerEntity):
media_content_type, media_content_type,
media_content_id, 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)

View File

@ -81,6 +81,7 @@ def mock_api() -> MagicMock:
jf_api.get_item.side_effect = api_get_item_side_effect 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.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.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 return jf_api

View File

@ -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( async def test_new_client_connected(
hass: HomeAssistant, hass: HomeAssistant,
init_integration: MockConfigEntry, init_integration: MockConfigEntry,