diff --git a/homeassistant/components/jellyfin/const.py b/homeassistant/components/jellyfin/const.py index d8379859e54..1f679fd43c8 100644 --- a/homeassistant/components/jellyfin/const.py +++ b/homeassistant/components/jellyfin/const.py @@ -7,7 +7,6 @@ DOMAIN: Final = "jellyfin" CLIENT_VERSION: Final = "1.0" COLLECTION_TYPE_MOVIES: Final = "movies" -COLLECTION_TYPE_TVSHOWS: Final = "tvshows" COLLECTION_TYPE_MUSIC: Final = "music" DATA_CLIENT: Final = "client" @@ -24,6 +23,7 @@ ITEM_TYPE_ALBUM: Final = "MusicAlbum" ITEM_TYPE_ARTIST: Final = "MusicArtist" ITEM_TYPE_AUDIO: Final = "Audio" ITEM_TYPE_LIBRARY: Final = "CollectionFolder" +ITEM_TYPE_MOVIE: Final = "Movie" MAX_IMAGE_WIDTH: Final = 500 MAX_STREAMING_BITRATE: Final = "140000000" @@ -33,8 +33,9 @@ MEDIA_SOURCE_KEY_PATH: Final = "Path" MEDIA_TYPE_AUDIO: Final = "Audio" MEDIA_TYPE_NONE: Final = "" +MEDIA_TYPE_VIDEO: Final = "Video" -SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC] +SUPPORTED_COLLECTION_TYPES: Final = [COLLECTION_TYPE_MUSIC, COLLECTION_TYPE_MOVIES] USER_APP_NAME: Final = "Home Assistant" USER_AGENT: Final = f"Home-Assistant/{CLIENT_VERSION}" diff --git a/homeassistant/components/jellyfin/manifest.json b/homeassistant/components/jellyfin/manifest.json index 993d2520484..48f4cf0c837 100644 --- a/homeassistant/components/jellyfin/manifest.json +++ b/homeassistant/components/jellyfin/manifest.json @@ -3,7 +3,7 @@ "name": "Jellyfin", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/jellyfin", - "requirements": ["jellyfin-apiclient-python==1.7.2"], + "requirements": ["jellyfin-apiclient-python==1.8.1"], "iot_class": "local_polling", "codeowners": ["@j-stienstra"], "loggers": ["jellyfin_apiclient_python"] diff --git a/homeassistant/components/jellyfin/media_source.py b/homeassistant/components/jellyfin/media_source.py index dbd79612378..879f4a4d4c8 100644 --- a/homeassistant/components/jellyfin/media_source.py +++ b/homeassistant/components/jellyfin/media_source.py @@ -4,7 +4,6 @@ from __future__ import annotations import logging import mimetypes from typing import Any -import urllib.parse from jellyfin_apiclient_python.api import jellyfin_url from jellyfin_apiclient_python.client import JellyfinClient @@ -13,6 +12,7 @@ from homeassistant.components.media_player.const import ( MEDIA_CLASS_ALBUM, MEDIA_CLASS_ARTIST, MEDIA_CLASS_DIRECTORY, + MEDIA_CLASS_MOVIE, MEDIA_CLASS_TRACK, ) from homeassistant.components.media_player.errors import BrowseError @@ -25,6 +25,7 @@ from homeassistant.components.media_source.models import ( from homeassistant.core import HomeAssistant from .const import ( + COLLECTION_TYPE_MOVIES, COLLECTION_TYPE_MUSIC, DATA_CLIENT, DOMAIN, @@ -39,11 +40,12 @@ from .const import ( ITEM_TYPE_ARTIST, ITEM_TYPE_AUDIO, ITEM_TYPE_LIBRARY, + ITEM_TYPE_MOVIE, MAX_IMAGE_WIDTH, - MAX_STREAMING_BITRATE, MEDIA_SOURCE_KEY_PATH, MEDIA_TYPE_AUDIO, MEDIA_TYPE_NONE, + MEDIA_TYPE_VIDEO, SUPPORTED_COLLECTION_TYPES, ) @@ -147,6 +149,8 @@ class JellyfinSource(MediaSource): if collection_type == COLLECTION_TYPE_MUSIC: return await self._build_music_library(library, include_children) + if collection_type == COLLECTION_TYPE_MOVIES: + return await self._build_movie_library(library, include_children) raise BrowseError(f"Unsupported collection type {collection_type}") @@ -270,6 +274,55 @@ class JellyfinSource(MediaSource): return result + async def _build_movie_library( + self, library: dict[str, Any], include_children: bool + ) -> BrowseMediaSource: + """Return a single movie 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=MEDIA_CLASS_DIRECTORY, + media_content_type=MEDIA_TYPE_NONE, + title=library_name, + can_play=False, + can_expand=True, + ) + + if include_children: + result.children_media_class = MEDIA_CLASS_MOVIE + result.children = await self._build_movies(library_id) # type: ignore[assignment] + + return result + + async def _build_movies(self, library_id: str) -> list[BrowseMediaSource]: + """Return all movies in the movie library.""" + movies = await self._get_children(library_id, ITEM_TYPE_MOVIE) + movies = sorted(movies, key=lambda k: k[ITEM_KEY_NAME]) # type: ignore[no-any-return] + return [self._build_movie(movie) for movie in movies] + + def _build_movie(self, movie: dict[str, Any]) -> BrowseMediaSource: + """Return a single movie as a browsable media source.""" + movie_id = movie[ITEM_KEY_ID] + movie_title = movie[ITEM_KEY_NAME] + mime_type = _media_mime_type(movie) + thumbnail_url = self._get_thumbnail_url(movie) + + result = BrowseMediaSource( + domain=DOMAIN, + identifier=movie_id, + media_class=MEDIA_CLASS_MOVIE, + media_content_type=mime_type, + title=movie_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]]: @@ -279,7 +332,7 @@ class JellyfinSource(MediaSource): "ParentId": parent_id, "IncludeItemTypes": item_type, } - if item_type == ITEM_TYPE_AUDIO: + if item_type in {ITEM_TYPE_AUDIO, ITEM_TYPE_MOVIE}: params["Fields"] = ITEM_KEY_MEDIA_SOURCES result = await self.hass.async_add_executor_job(self.api.user_items, "", params) @@ -298,29 +351,15 @@ class JellyfinSource(MediaSource): def _get_stream_url(self, media_item: dict[str, Any]) -> str: """Return the stream URL for a media item.""" media_type = media_item[ITEM_KEY_MEDIA_TYPE] + item_id = media_item[ITEM_KEY_ID] if media_type == MEDIA_TYPE_AUDIO: - return self._get_audio_stream_url(media_item) + return self.api.audio_url(item_id) # type: ignore[no-any-return] + if media_type == MEDIA_TYPE_VIDEO: + return self.api.video_url(item_id) # type: ignore[no-any-return] raise BrowseError(f"Unsupported media type {media_type}") - def _get_audio_stream_url(self, media_item: dict[str, Any]) -> str: - """Return the stream URL for a music media item.""" - item_id = media_item[ITEM_KEY_ID] - user_id = self.client.config.data["auth.user_id"] - device_id = self.client.config.data["app.device_id"] - api_key = self.client.config.data["auth.token"] - - params = urllib.parse.urlencode( - { - "UserId": user_id, - "DeviceId": device_id, - "api_key": api_key, - "MaxStreamingBitrate": MAX_STREAMING_BITRATE, - } - ) - return f"{self.url}Audio/{item_id}/universal?{params}" - def _media_mime_type(media_item: dict[str, Any]) -> str: """Return the mime type of a media item.""" diff --git a/requirements_all.txt b/requirements_all.txt index edb9131f9a5..2bea1a28b50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -903,7 +903,7 @@ iperf3==0.1.11 ismartgate==4.0.4 # homeassistant.components.jellyfin -jellyfin-apiclient-python==1.7.2 +jellyfin-apiclient-python==1.8.1 # homeassistant.components.rest jsonpath==0.82 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b3ca7cf27b1..15242eaad94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -643,7 +643,7 @@ iotawattpy==0.1.0 ismartgate==4.0.4 # homeassistant.components.jellyfin -jellyfin-apiclient-python==1.7.2 +jellyfin-apiclient-python==1.8.1 # homeassistant.components.rest jsonpath==0.82