Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions homeassistant/components/kodi/browse_media.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
)
2 changes: 1 addition & 1 deletion homeassistant/components/kodi/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."]
}
15 changes: 15 additions & 0 deletions homeassistant/components/kodi/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
SearchMedia,
SearchMediaQuery,
async_process_play_media_url,
)
from homeassistant.const import (
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading