Add Jellyfin media source support for tvshows (#85953)
This commit is contained in:
parent
fe583b7c4a
commit
a35a4efaaa
3 changed files with 146 additions and 6 deletions
|
@ -11,7 +11,12 @@ from homeassistant.components.media_player.browse_media import BrowseMedia
|
|||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .client_wrapper import get_artwork_url
|
||||
from .const import CONTENT_TYPE_MAP, MEDIA_CLASS_MAP, MEDIA_TYPE_NONE
|
||||
from .const import (
|
||||
CONTENT_TYPE_MAP,
|
||||
MEDIA_CLASS_MAP,
|
||||
MEDIA_TYPE_NONE,
|
||||
SUPPORTED_COLLECTION_TYPES,
|
||||
)
|
||||
|
||||
CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS: dict[str, str] = {
|
||||
MediaType.MUSIC: MediaClass.MUSIC,
|
||||
|
@ -22,8 +27,6 @@ CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS: dict[str, str] = {
|
|||
"library": MediaClass.DIRECTORY,
|
||||
}
|
||||
|
||||
JF_SUPPORTED_LIBRARY_TYPES = ["movies", "music", "tvshows"]
|
||||
|
||||
PLAYABLE_MEDIA_TYPES = [
|
||||
MediaType.EPISODE,
|
||||
MediaType.MOVIE,
|
||||
|
@ -65,7 +68,7 @@ async def build_root_response(
|
|||
children = [
|
||||
await item_payload(hass, client, user_id, folder)
|
||||
for folder in folders["Items"]
|
||||
if folder["CollectionType"] in JF_SUPPORTED_LIBRARY_TYPES
|
||||
if folder["CollectionType"] in SUPPORTED_COLLECTION_TYPES
|
||||
]
|
||||
|
||||
return BrowseMedia(
|
||||
|
|
|
@ -11,6 +11,7 @@ CLIENT_VERSION: Final = hass_version
|
|||
|
||||
COLLECTION_TYPE_MOVIES: Final = "movies"
|
||||
COLLECTION_TYPE_MUSIC: Final = "music"
|
||||
COLLECTION_TYPE_TVSHOWS: Final = "tvshows"
|
||||
|
||||
CONF_CLIENT_DEVICE_ID: Final = "client_device_id"
|
||||
|
||||
|
@ -27,8 +28,11 @@ ITEM_KEY_NAME: Final = "Name"
|
|||
ITEM_TYPE_ALBUM: Final = "MusicAlbum"
|
||||
ITEM_TYPE_ARTIST: Final = "MusicArtist"
|
||||
ITEM_TYPE_AUDIO: Final = "Audio"
|
||||
ITEM_TYPE_EPISODE: Final = "Episode"
|
||||
ITEM_TYPE_LIBRARY: Final = "CollectionFolder"
|
||||
ITEM_TYPE_MOVIE: Final = "Movie"
|
||||
ITEM_TYPE_SERIES: Final = "Series"
|
||||
ITEM_TYPE_SEASON: Final = "Season"
|
||||
|
||||
MAX_IMAGE_WIDTH: Final = 500
|
||||
MAX_STREAMING_BITRATE: Final = "140000000"
|
||||
|
@ -39,7 +43,14 @@ MEDIA_TYPE_AUDIO: Final = "Audio"
|
|||
MEDIA_TYPE_NONE: Final = ""
|
||||
MEDIA_TYPE_VIDEO: Final = "Video"
|
||||
|
||||
SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC, COLLECTION_TYPE_MOVIES]
|
||||
SUPPORTED_COLLECTION_TYPES: Final = [
|
||||
COLLECTION_TYPE_MUSIC,
|
||||
COLLECTION_TYPE_MOVIES,
|
||||
COLLECTION_TYPE_TVSHOWS,
|
||||
]
|
||||
|
||||
PLAYABLE_ITEM_TYPES: Final = [ITEM_TYPE_AUDIO, ITEM_TYPE_EPISODE, ITEM_TYPE_MOVIE]
|
||||
|
||||
|
||||
USER_APP_NAME: Final = "Home Assistant"
|
||||
USER_AGENT: Final = f"Home-Assistant/{CLIENT_VERSION}"
|
||||
|
|
|
@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant
|
|||
from .const import (
|
||||
COLLECTION_TYPE_MOVIES,
|
||||
COLLECTION_TYPE_MUSIC,
|
||||
COLLECTION_TYPE_TVSHOWS,
|
||||
DOMAIN,
|
||||
ITEM_KEY_COLLECTION_TYPE,
|
||||
ITEM_KEY_ID,
|
||||
|
@ -32,13 +33,17 @@ from .const import (
|
|||
ITEM_TYPE_ALBUM,
|
||||
ITEM_TYPE_ARTIST,
|
||||
ITEM_TYPE_AUDIO,
|
||||
ITEM_TYPE_EPISODE,
|
||||
ITEM_TYPE_LIBRARY,
|
||||
ITEM_TYPE_MOVIE,
|
||||
ITEM_TYPE_SEASON,
|
||||
ITEM_TYPE_SERIES,
|
||||
MAX_IMAGE_WIDTH,
|
||||
MEDIA_SOURCE_KEY_PATH,
|
||||
MEDIA_TYPE_AUDIO,
|
||||
MEDIA_TYPE_NONE,
|
||||
MEDIA_TYPE_VIDEO,
|
||||
PLAYABLE_ITEM_TYPES,
|
||||
SUPPORTED_COLLECTION_TYPES,
|
||||
)
|
||||
from .models import JellyfinData
|
||||
|
@ -100,6 +105,10 @@ class JellyfinSource(MediaSource):
|
|||
return await self._build_artist(media_item, True)
|
||||
if item_type == ITEM_TYPE_ALBUM:
|
||||
return await self._build_album(media_item, True)
|
||||
if item_type == ITEM_TYPE_SERIES:
|
||||
return await self._build_series(media_item, True)
|
||||
if item_type == ITEM_TYPE_SEASON:
|
||||
return await self._build_season(media_item, True)
|
||||
|
||||
raise BrowseError(f"Unsupported item type {item_type}")
|
||||
|
||||
|
@ -146,6 +155,8 @@ class JellyfinSource(MediaSource):
|
|||
return await self._build_music_library(library, include_children)
|
||||
if collection_type == COLLECTION_TYPE_MOVIES:
|
||||
return await self._build_movie_library(library, include_children)
|
||||
if collection_type == COLLECTION_TYPE_TVSHOWS:
|
||||
return await self._build_tv_library(library, include_children)
|
||||
|
||||
raise BrowseError(f"Unsupported collection type {collection_type}")
|
||||
|
||||
|
@ -326,6 +337,121 @@ class JellyfinSource(MediaSource):
|
|||
|
||||
return result
|
||||
|
||||
async def _build_tv_library(
|
||||
self, library: dict[str, Any], include_children: bool
|
||||
) -> BrowseMediaSource:
|
||||
"""Return a single tv show library as a browsable media source."""
|
||||
library_id = library[ITEM_KEY_ID]
|
||||
library_name = library[ITEM_KEY_NAME]
|
||||
|
||||
result = BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=library_id,
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MEDIA_TYPE_NONE,
|
||||
title=library_name,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
|
||||
if include_children:
|
||||
result.children_media_class = MediaClass.TV_SHOW
|
||||
result.children = await self._build_tvshow(library_id)
|
||||
|
||||
return result
|
||||
|
||||
async def _build_tvshow(self, library_id: str) -> list[BrowseMediaSource]:
|
||||
"""Return all series in the tv library."""
|
||||
series = await self._get_children(library_id, ITEM_TYPE_SERIES)
|
||||
series = sorted(series, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return]
|
||||
return [await self._build_series(serie, False) for serie in series]
|
||||
|
||||
async def _build_series(
|
||||
self, series: dict[str, Any], include_children: bool
|
||||
) -> BrowseMediaSource:
|
||||
"""Return a single series as a browsable media source."""
|
||||
series_id = series[ITEM_KEY_ID]
|
||||
series_title = series[ITEM_KEY_NAME]
|
||||
thumbnail_url = self._get_thumbnail_url(series)
|
||||
|
||||
result = BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=series_id,
|
||||
media_class=MediaClass.TV_SHOW,
|
||||
media_content_type=MEDIA_TYPE_NONE,
|
||||
title=series_title,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
thumbnail=thumbnail_url,
|
||||
)
|
||||
|
||||
if include_children:
|
||||
result.children_media_class = MediaClass.SEASON
|
||||
result.children = await self._build_seasons(series_id)
|
||||
|
||||
return result
|
||||
|
||||
async def _build_seasons(self, series_id: str) -> list[BrowseMediaSource]:
|
||||
"""Return all seasons in the series."""
|
||||
seasons = await self._get_children(series_id, ITEM_TYPE_SEASON)
|
||||
seasons = sorted(seasons, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return]
|
||||
return [await self._build_season(season, False) for season in seasons]
|
||||
|
||||
async def _build_season(
|
||||
self, season: dict[str, Any], include_children: bool
|
||||
) -> BrowseMediaSource:
|
||||
"""Return a single series as a browsable media source."""
|
||||
season_id = season[ITEM_KEY_ID]
|
||||
season_title = season[ITEM_KEY_NAME]
|
||||
thumbnail_url = self._get_thumbnail_url(season)
|
||||
|
||||
result = BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=season_id,
|
||||
media_class=MediaClass.TV_SHOW,
|
||||
media_content_type=MEDIA_TYPE_NONE,
|
||||
title=season_title,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
thumbnail=thumbnail_url,
|
||||
)
|
||||
|
||||
if include_children:
|
||||
result.children_media_class = MediaClass.EPISODE
|
||||
result.children = await self._build_episodes(season_id)
|
||||
|
||||
return result
|
||||
|
||||
async def _build_episodes(self, season_id: str) -> list[BrowseMediaSource]:
|
||||
"""Return all episode in the season."""
|
||||
episodes = await self._get_children(season_id, ITEM_TYPE_EPISODE)
|
||||
episodes = sorted(episodes, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return]
|
||||
return [
|
||||
self._build_episode(episode)
|
||||
for episode in episodes
|
||||
if _media_mime_type(episode) is not None
|
||||
]
|
||||
|
||||
def _build_episode(self, episode: dict[str, Any]) -> BrowseMediaSource:
|
||||
"""Return a single episode as a browsable media source."""
|
||||
episode_id = episode[ITEM_KEY_ID]
|
||||
episode_title = episode[ITEM_KEY_NAME]
|
||||
mime_type = _media_mime_type(episode)
|
||||
thumbnail_url = self._get_thumbnail_url(episode)
|
||||
|
||||
result = BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=episode_id,
|
||||
media_class=MediaClass.EPISODE,
|
||||
media_content_type=mime_type,
|
||||
title=episode_title,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=thumbnail_url,
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def _get_children(
|
||||
self, parent_id: str, item_type: str
|
||||
) -> list[dict[str, Any]]:
|
||||
|
@ -335,7 +461,7 @@ class JellyfinSource(MediaSource):
|
|||
"ParentId": parent_id,
|
||||
"IncludeItemTypes": item_type,
|
||||
}
|
||||
if item_type in {ITEM_TYPE_AUDIO, ITEM_TYPE_MOVIE}:
|
||||
if item_type in PLAYABLE_ITEM_TYPES:
|
||||
params["Fields"] = ITEM_KEY_MEDIA_SOURCES
|
||||
|
||||
result = await self.hass.async_add_executor_job(self.api.user_items, "", params)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue