Map Jellyfin search filter classes to item types (#183252)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paulus Schoutsen
2026-09-27 14:47:22 +02:00
committed by GitHub
co-authored by Claude
parent 2125b81943
commit ae965b48b6
3 changed files with 79 additions and 2 deletions
@@ -20,6 +20,7 @@ from .const import (
CONTENT_TYPE_MAP,
MEDIA_CLASS_MAP,
MEDIA_TYPE_NONE,
SEARCH_ITEM_TYPE_MAP,
SUPPORTED_COLLECTION_TYPES,
)
@@ -165,9 +166,18 @@ async def search_items(
items: list[dict[str, Any]] = []
# Search for items based on media filter classes (or all if none specified)
media_types: list[MediaClass] | list[None] = []
media_types: list[str] | list[None] = []
if query.media_filter_classes:
media_types = query.media_filter_classes
# Jellyfin ignores unknown item types and returns unfiltered results,
# so skip classes that have no Jellyfin item type. Classes can share an
# item type, so search each item type once.
media_types = list(
dict.fromkeys(
",".join(SEARCH_ITEM_TYPE_MAP[media_class])
for media_class in query.media_filter_classes
if media_class in SEARCH_ITEM_TYPE_MAP
)
)
else:
media_types = [None]
@@ -82,7 +82,18 @@ MEDIA_CLASS_MAP = {
"BoxSet": MediaClass.DIRECTORY,
"Episode": MediaClass.EPISODE,
"Season": MediaClass.SEASON,
"Playlist": MediaClass.PLAYLIST,
"Video": MediaClass.VIDEO,
}
# The track class has no item type of its own, so it searches Audio items too.
SEARCH_ITEM_TYPE_MAP: dict[MediaClass, list[str]] = {
media_class: [
item_type
for item_type, item_class in MEDIA_CLASS_MAP.items()
if item_class == media_class
]
for media_class in set(MEDIA_CLASS_MAP.values())
} | {MediaClass.TRACK: [ITEM_TYPE_AUDIO]}
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE, Platform.SENSOR]
LOGGER = logging.getLogger(__package__)
@@ -3,6 +3,8 @@
from datetime import timedelta
from unittest.mock import MagicMock
import pytest
from homeassistant.components.jellyfin.const import DOMAIN
from homeassistant.components.media_player import (
ATTR_MEDIA_ALBUM_ARTIST,
@@ -531,6 +533,60 @@ async def test_search_media(
]
@pytest.mark.parametrize(
("media_filter_classes", "expected_item_types"),
[
pytest.param([], [None], id="no_filter"),
pytest.param(["album"], ["MusicAlbum"], id="album"),
pytest.param(["artist"], ["MusicArtist"], id="artist"),
pytest.param(["track"], ["Audio"], id="track"),
pytest.param(["movie"], ["Movie"], id="movie"),
pytest.param(["playlist"], ["Playlist"], id="playlist"),
pytest.param(["video"], ["Video"], id="video"),
pytest.param(
["directory"],
["CollectionFolder,AggregateFolder,Folder,BoxSet"],
id="multiple_item_types",
),
pytest.param(
["album", "tv_show"], ["MusicAlbum", "Series"], id="multiple_classes"
),
pytest.param(["music", "track"], ["Audio"], id="shared_item_type"),
pytest.param(["podcast"], [], id="unmapped_class"),
],
)
@pytest.mark.usefixtures("init_integration")
async def test_search_media_item_types(
hass_ws_client: WebSocketGenerator,
mock_api: MagicMock,
media_filter_classes: list[str],
expected_item_types: list[str | None],
) -> None:
"""Test Jellyfin search maps media filter classes to Jellyfin item types."""
client = await hass_ws_client()
await client.send_json_auto_id(
{
"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": media_filter_classes,
}
)
response = await client.receive_json()
assert response["success"]
# The filter classes arrive as a set, so the search order is not fixed.
assert sorted(
(
search_call.kwargs["media"]
for search_call in mock_api.search_media_items.call_args_list
),
key=str,
) == sorted(expected_item_types, key=str)
async def test_new_client_connected(
hass: HomeAssistant,
init_integration: MockConfigEntry,