diff --git a/homeassistant/components/kodi/browse_media.py b/homeassistant/components/kodi/browse_media.py index b62379aaa253c9..22dee004d76f5e 100644 --- a/homeassistant/components/kodi/browse_media.py +++ b/homeassistant/components/kodi/browse_media.py @@ -4,13 +4,17 @@ import contextlib import logging +from rapidfuzz import fuzz, process, utils + from homeassistant.components import media_source from homeassistant.components.media_player import ( BrowseError, BrowseMedia, MediaClass, MediaType, + SearchMediaQuery, ) +from homeassistant.helpers.network import is_internal_request PLAYABLE_MEDIA_TYPES = [ MediaType.ALBUM, @@ -360,3 +364,100 @@ async def get_media_info(media_library, search_id, search_type): title = "Channels" return thumbnail, title, media + + +async def search_items( + hass, media_library, query: SearchMediaQuery, get_browse_image_url +) -> list[BrowseMedia]: + """Search the items for the query.""" + + media_filter_classes = query.media_filter_classes or [] + media_filter_classes_supported_for_search = [ + MediaClass.MOVIE, + MediaClass.TV_SHOW, + ] + + is_internal = is_internal_request(hass) + + media_filter_classes = [ + t + for t in media_filter_classes + if t in media_filter_classes_supported_for_search + ] + + if not media_filter_classes: + media_filter_classes = media_filter_classes_supported_for_search + + if query.media_content_id and media_source.is_media_source_id( + query.media_content_id + ): + result = await media_source.async_browse_media( + hass, query.media_content_id, content_filter=media_source_content_filter + ) + return [result] + + results = [] + + if MediaClass.MOVIE in media_filter_classes: + movies = (await media_library.get_movies()).get("movies", []) + movies_names = [i["label"] for i in movies] + matching_movie_indices = process.extract( + query.search_query, + movies_names, + scorer=fuzz.WRatio, + processor=utils.default_process, + score_cutoff=80, + limit=None, + ) + + matching_movies = [ + movies[idx] for (label, fuzz_score, idx) in matching_movie_indices + ] + + results.extend(matching_movies) + + if MediaClass.TV_SHOW in media_filter_classes: + tvshows = (await media_library.get_tv_shows()).get("tvshows", []) + tvshow_names = {i["tvshowid"]: i["label"] for i in tvshows} + matching_tvshow_ids = process.extract( + query.search_query, + tvshow_names, + scorer=fuzz.WRatio, + processor=utils.default_process, + score_cutoff=80, + limit=None, + ) + + matching_tvshow_ids = [ + tvshowid for (label, fuzz_score, tvshowid) in matching_tvshow_ids + ] + + for tvshowid in matching_tvshow_ids: + episodes = ( + await media_library.get_episodes( + tvshowid, 0, properties=["playcount", "firstaired"] + ) + )["episodes"] + episodes = [ep for ep in episodes if not ep["playcount"]] + episodes.sort(key=lambda m: m["firstaired"]) + if episodes: + results.append(episodes[0]) + + async def _get_thumbnail_url( + media_content_type, + media_content_id, + media_image_id=None, + thumbnail_url=None, + ): + if is_internal: + return media_library.thumbnail_url(thumbnail_url) + + return get_browse_image_url( + media_content_type, + media_content_id, + media_image_id, + ) + + return await asyncio.gather( + *(item_payload(item, _get_thumbnail_url) for item in results) + ) diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 7a36c240ff60ef..7d3a2821334a80 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -8,6 +8,6 @@ "integration_type": "service", "iot_class": "local_push", "loggers": ["jsonrpc_async", "jsonrpc_base", "jsonrpc_websocket", "pykodi"], - "requirements": ["pykodi==0.2.7"], + "requirements": ["pykodi==0.2.7", "rapidfuzz==3.14.3"], "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."] } diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 52030ec74f2a42..07a94d4b5b0d30 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -20,6 +20,8 @@ MediaPlayerEntityFeature, MediaPlayerState, MediaType, + SearchMedia, + SearchMediaQuery, async_process_play_media_url, ) from homeassistant.const import ( @@ -43,6 +45,7 @@ get_media_info, library_payload, media_source_content_filter, + search_items, ) from .const import DOMAIN, EVENT_TURN_OFF, EVENT_TURN_ON @@ -128,6 +131,7 @@ class KodiEntity(MediaPlayerEntity): _attr_translation_key = "media_player" _attr_supported_features = ( MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.SEARCH_MEDIA | MediaPlayerEntityFeature.NEXT_TRACK | MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY @@ -825,6 +829,17 @@ async def _get_thumbnail_url( ) return response + async def async_search_media( + self, + query: SearchMediaQuery, + ) -> SearchMedia: + """Search the media player.""" + + media = await search_items( + self.hass, self._kodi, query, self.get_browse_image_url + ) + return SearchMedia(result=media) + async def async_get_browse_image( self, media_content_type: MediaType | str, diff --git a/requirements_all.txt b/requirements_all.txt index 850adbe2acabf3..e2f35e8ebbf55b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2755,6 +2755,9 @@ radiotherm==2.1.0 # homeassistant.components.raincloud raincloudy==0.0.7 +# homeassistant.components.kodi +rapidfuzz==3.14.3 + # homeassistant.components.rapt_ble rapt-ble==0.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fa1a22b48cc414..8824250f824ffc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2327,6 +2327,9 @@ radios==0.3.2 # homeassistant.components.radiotherm radiotherm==2.1.0 +# homeassistant.components.kodi +rapidfuzz==3.14.3 + # homeassistant.components.rapt_ble rapt-ble==0.1.2